[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / purchase / purchase.py
index de208f7..905296b 100644 (file)
@@ -28,8 +28,9 @@ from openerp.tools.safe_eval import safe_eval as eval
 from openerp.osv import fields, osv
 from openerp.tools.translate import _
 import openerp.addons.decimal_precision as dp
-from openerp.osv.orm import browse_record, browse_null
+from openerp.osv.orm import browse_record_list, browse_record, browse_null
 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
+from openerp.tools.float_utils import float_compare
 
 class purchase_order(osv.osv):
 
@@ -66,6 +67,7 @@ class purchase_order(osv.osv):
                         (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
             cr.execute("""update purchase_order set
                     minimum_planned_date=%s where id=%s""", (value, po.id))
+        self.invalidate_cache(cr, uid, context=context)
         return True
 
     def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
@@ -148,7 +150,15 @@ class purchase_order(osv.osv):
 
     def _get_picking_in(self, cr, uid, context=None):
         obj_data = self.pool.get('ir.model.data')
-        return obj_data.get_object_reference(cr, uid, 'stock','picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock','picking_type_in')[1] or False
+        type_obj = self.pool.get('stock.picking.type')
+        user_obj = self.pool.get('res.users')
+        company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id
+        types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context)
+        if not types:
+            types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context)
+            if not types:
+                raise osv.except_osv(_('Error!'), _("Make sure you have at least an incoming picking type defined"))
+        return types[0]
 
     def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
         res = {}
@@ -166,6 +176,15 @@ class purchase_order(osv.osv):
             res[po_id].append(pick_id)
         return res
 
+    def _count_all(self, cr, uid, ids, field_name, arg, context=None):
+        return {
+            purchase.id: {
+                'shipment_count': len(purchase.picking_ids),
+                'invoice_count': len(purchase.invoice_ids),                
+            }
+            for purchase in self.browse(cr, uid, ids, context=context)
+        }
+
     STATE_SELECTION = [
         ('draft', 'Draft PO'),
         ('sent', 'RFQ'),
@@ -185,14 +204,26 @@ class purchase_order(osv.osv):
         },
     }
     _columns = {
-        'name': fields.char('Order Reference', size=64, required=True, select=True, help="Unique number of the purchase order, computed automatically when the purchase order is created."),
-        'origin': fields.char('Source Document', size=64,
-            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 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"),
+        'name': fields.char('Order Reference', required=True, select=True, copy=False,
+                            help="Unique number of the purchase order, "
+                                 "computed automatically when the purchase order is created."),
+        'origin': fields.char('Source Document', copy=False,
+                              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)]},
+                                   copy=False,
+                                   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.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)],
+                                                                      'approved':[('readonly',True)]},
+                                 select=True, help="Depicts the date where the Quotation should be validated and converted into a Purchase Order, by default it's the creation date.",
+                                 copy=False),
+        'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False,
+                                   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)]},
             change_default=True, track_visibility='always'),
         'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
@@ -203,21 +234,37 @@ 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 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),
+        '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 receipt of goods, the status becomes "
+                                       "in exception.",
+                                  select=True, copy=False),
+        'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines',
+                                      states={'approved':[('readonly',True)],
+                                              'done':[('readonly',True)]},
+                                      copy=True),
+        'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False),
         'notes': fields.text('Terms and Conditions'),
-        'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
-        'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of reception operations that have been generated for this purchase order."),
-        'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
+        'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id',
+                                        'invoice_id', 'Invoices', copy=False,
+                                        help="Invoices generated for a purchase order"),
+        'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of receipts that have been generated for this purchase order."),
+        'shipped':fields.boolean('Received', readonly=True, select=True, copy=False,
+                                 help="It indicates that a picking has been done"),
         'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
-        'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been paid"),
+        'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False,
+                                    help="It indicates that an invoice has been validated"),
         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
         'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
-            readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
+            readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)],'bid':[('readonly',False)]},
             help="Based on Purchase Order lines: place individual lines in 'Invoice Control / On Purchase Order lines' from where you can selectively create an invoice.\n" \
                 "Based on generated invoice: create a draft invoice you can validate later.\n" \
-                "Based on incoming shipments: let you create an invoice when receptions are validated."
+                "Based on incoming shipments: let you create an invoice when receipts are validated."
         ),
         'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
             store = {
@@ -248,9 +295,11 @@ class purchase_order(osv.osv):
         'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True,
                                            states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}),
         'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True),        
+        'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True),
+        'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True)
     }
     _defaults = {
-        'date_order': fields.date.context_today,
+        'date_order': fields.datetime.now,
         'state': 'draft',
         'name': lambda obj, cr, uid, context: '/',
         'shipped': 0,
@@ -272,10 +321,8 @@ class purchase_order(osv.osv):
 
     def create(self, cr, uid, vals, context=None):
         if vals.get('name','/')=='/':
-            vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
-        if context is None:
-            context = {}
-        context.update({'mail_create_nolog': True})
+            vals['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'purchase.order') or '/'
+        context = dict(context or {}, mail_create_nolog=True)
         order =  super(purchase_order, self).create(cr, uid, vals, context=context)
         self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
         return order
@@ -290,7 +337,7 @@ class purchase_order(osv.osv):
                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
 
         # automatically sending subflow.delete upon deletion
-        self.signal_purchase_cancel(cr, uid, unlink_ids)
+        self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel')
 
         return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
 
@@ -316,13 +363,13 @@ class purchase_order(osv.osv):
             return {}
         return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
 
-   #Destination address is used when dropshipping 
-    def onchange_dest_address_id(self, cr, uid, ids, address_id):
+    #Destination address is used when dropshipping
+    def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None):
         if not address_id:
             return {}
         address = self.pool.get('res.partner')
         values = {}
-        supplier = address.browse(cr, uid, address_id)
+        supplier = address.browse(cr, uid, address_id, context=context)
         if supplier:
             location_id = supplier.property_stock_customer.id
             values.update({'location_id': location_id})
@@ -337,15 +384,15 @@ class purchase_order(osv.osv):
             value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
         return {'value': value}
 
-    def onchange_partner_id(self, cr, uid, ids, partner_id):
+    def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
         partner = self.pool.get('res.partner')
         if not partner_id:
             return {'value': {
                 'fiscal_position': False,
                 'payment_term_id': False,
                 }}
-        supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
-        supplier = partner.browse(cr, uid, partner_id)
+        supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context)
+        supplier = partner.browse(cr, uid, partner_id, context=context)
         return {'value': {
             'pricelist_id': supplier.property_product_pricelist_purchase.id,
             'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
@@ -377,6 +424,7 @@ class purchase_order(osv.osv):
         '''
         This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
         '''
+        context = dict(context or {})
         mod_obj = self.pool.get('ir.model.data')
         wizard_obj = self.pool.get('purchase.order.line_invoice')
         #compute the number of invoices to display
@@ -416,7 +464,6 @@ class purchase_order(osv.osv):
         action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context)
 
         pick_ids = []
-        #TODO: might need to change this function in order to return the whole set of operations and not only the reception(s) to input
         for po in self.browse(cr, uid, ids, context=context):
             pick_ids += [picking.id for picking in po.picking_ids]
 
@@ -481,10 +528,9 @@ class purchase_order(osv.osv):
         This function prints the request for quotation and mark it as sent, so that we can see more easily the next step of the workflow
         '''
         assert len(ids) == 1, 'This option should only be used for a single id at a time'
-        self.signal_send_rfq(cr, uid, ids)
+        self.signal_workflow(cr, uid, ids, 'send_rfq')
         return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
 
-    #TODO: implement messages system
     def wkf_confirm_order(self, cr, uid, ids, context=None):
         todo = []
         for po in self.browse(cr, uid, ids, context=context):
@@ -506,7 +552,7 @@ class purchase_order(osv.osv):
             if not acc_id:
                 acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
             if not acc_id:
-                raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
+                raise osv.except_osv(_('Error!'), _('Define an expense account for this product: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
         else:
             acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
         fpos = po_line.order_id.fiscal_position or False
@@ -532,6 +578,41 @@ class purchase_order(osv.osv):
             'purchase_line_id': order_line.id,
         }
 
+    def _prepare_invoice(self, cr, uid, order, line_ids, context=None):
+        """Prepare the dict of values to create the new invoice for a
+           purchase order. This method may be overridden to implement custom
+           invoice generation (making sure to call super() to establish
+           a clean extension chain).
+
+           :param browse_record order: purchase.order record to invoice
+           :param list(int) line_ids: list of invoice line IDs that must be
+                                      attached to the invoice
+           :return: dict of value to create() the invoice
+        """
+        journal_ids = self.pool['account.journal'].search(
+                            cr, uid, [('type', '=', 'purchase'),
+                                      ('company_id', '=', order.company_id.id)],
+                            limit=1)
+        if not journal_ids:
+            raise osv.except_osv(
+                _('Error!'),
+                _('Define purchase journal for this company: "%s" (id:%d).') % \
+                    (order.company_id.name, order.company_id.id))
+        return {
+            'name': order.partner_ref or order.name,
+            'reference': order.partner_ref or order.name,
+            'account_id': order.partner_id.property_account_payable.id,
+            'type': 'in_invoice',
+            'partner_id': order.partner_id.id,
+            'currency_id': order.currency_id.id,
+            'journal_id': len(journal_ids) and journal_ids[0] or False,
+            'invoice_line': [(6, 0, line_ids)],
+            'origin': order.name,
+            'fiscal_position': order.fiscal_position.id or False,
+            'payment_term': order.payment_term_id.id or False,
+            'company_id': order.company_id.id,
+        }
+
     def action_cancel_draft(self, cr, uid, ids, context=None):
         if not len(ids):
             return False
@@ -553,9 +634,8 @@ class purchase_order(osv.osv):
         :return: ID of created invoice.
         :rtype: int
         """
-        if context is None:
-            context = {}
-        journal_obj = self.pool.get('account.journal')
+        context = dict(context or {})
+        
         inv_obj = self.pool.get('account.invoice')
         inv_line_obj = self.pool.get('account.invoice.line')
 
@@ -568,12 +648,7 @@ class purchase_order(osv.osv):
                 #then re-do a browse to read the property fields for the good company.
                 context['force_company'] = order.company_id.id
                 order = self.browse(cr, uid, order.id, context=context)
-            pay_acc_id = order.partner_id.property_account_payable.id
-            journal_ids = journal_obj.search(cr, uid, [('type', '=', 'purchase'), ('company_id', '=', order.company_id.id)], limit=1)
-            if not journal_ids:
-                raise osv.except_osv(_('Error!'),
-                    _('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
-
+            
             # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
             inv_lines = []
             for po_line in order.order_line:
@@ -581,31 +656,17 @@ class purchase_order(osv.osv):
                 inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
                 inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
                 inv_lines.append(inv_line_id)
-
-                po_line.write({'invoice_lines': [(4, inv_line_id)]}, context=context)
+                po_line.write({'invoice_lines': [(4, inv_line_id)]})
 
             # get invoice data and create invoice
-            inv_data = {
-                'name': order.partner_ref or order.name,
-                'reference': order.partner_ref or order.name,
-                'account_id': pay_acc_id,
-                'type': 'in_invoice',
-                'partner_id': order.partner_id.id,
-                'currency_id': order.currency_id.id,
-                'journal_id': len(journal_ids) and journal_ids[0] or False,
-                'invoice_line': [(6, 0, inv_lines)],
-                'origin': order.name,
-                'fiscal_position': order.fiscal_position.id or False,
-                'payment_term': order.payment_term_id.id or False,
-                'company_id': order.company_id.id,
-            }
+            inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context)
             inv_id = inv_obj.create(cr, uid, inv_data, context=context)
 
             # compute the invoice
             inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
 
             # Link this new invoice to related purchase order
-            order.write({'invoice_ids': [(4, inv_id)]}, context=context)
+            order.write({'invoice_ids': [(4, inv_id)]})
             res = inv_id
         return res
 
@@ -620,25 +681,27 @@ 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'}, context=context)
+        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:
-                if pick.state not in ('draft', 'cancel'):
-                    raise osv.except_osv(
-                        _('Unable to cancel the purchase order %s.') % (purchase.name),
-                        _('First cancel all receptions related to this purchase order.'))
-            self.pool.get('stock.picking') \
-                .signal_button_cancel(cr, uid, map(attrgetter('id'), purchase.picking_ids))
+                for move in pick.move_lines:
+                    if pick.state == 'done':
+                        raise osv.except_osv(
+                            _('Unable to cancel the purchase order %s.') % (purchase.name),
+                            _('You have already received some goods for it.  '))
+            self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context)
             for inv in purchase.invoice_ids:
                 if inv and inv.state not in ('cancel', 'draft'):
                     raise osv.except_osv(
                         _('Unable to cancel this purchase order.'),
                         _('You must first cancel all invoices related to this purchase order.'))
             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)
+                .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel')
+        self.signal_workflow(cr, uid, ids, 'purchase_cancel')
         return True
 
     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
@@ -646,7 +709,7 @@ class purchase_order(osv.osv):
         product_uom = self.pool.get('product.uom')
         price_unit = order_line.price_unit
         if order_line.product_uom.id != order_line.product_id.uom_id.id:
-            price_unit *= order_line.product_uom.factor
+            price_unit *= order_line.product_uom.factor / order_line.product_id.uom_id.factor
         if order.currency_id.id != order.company_id.currency_id.id:
             #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
             price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context)
@@ -656,7 +719,7 @@ class purchase_order(osv.osv):
             'product_id': order_line.product_id.id,
             'product_uom': order_line.product_uom.id,
             'product_uos': order_line.product_uom.id,
-            'date': fields.date.date_to_datetime(self, cr, uid, order.date_order, context),
+            'date': order.date_order,
             'date_expected': fields.date.date_to_datetime(self, cr, uid, order_line.date_planned, context),
             'location_id': order.partner_id.property_stock_supplier.id,
             'location_dest_id': order.location_id.id,
@@ -673,6 +736,7 @@ class purchase_order(osv.osv):
             'origin': order.name,
             'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [],
             'warehouse_id':order.picking_type_id.warehouse_id.id,
+            'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none',
         }
 
         diff_quantity = order_line.product_qty
@@ -682,9 +746,11 @@ class purchase_order(osv.osv):
             tmp.update({
                 'product_uom_qty': min(procurement_qty, diff_quantity),
                 'product_uos_qty': min(procurement_qty, diff_quantity),
-                'move_dest_id': procurement.move_dest_id.id,  # blabla
-                'group_id': procurement.group_id.id or group_id,  # blabla to check ca devrait etre bon et groupĂ© dans le meme picking qd meme
+                'move_dest_id': procurement.move_dest_id.id,  #move destination is same as procurement destination
+                'group_id': procurement.group_id.id or group_id,  #move group is same as group of procurements if it exists, otherwise take another group
                 'procurement_id': procurement.id,
+                'invoice_state': procurement.rule_id.invoice_state or (procurement.location_id and procurement.location_id.usage == 'customer' and procurement.invoice_state=='picking' and '2binvoiced') or (order.invoice_method == 'picking' and '2binvoiced') or 'none', #dropship case takes from sale
+                'propagate': procurement.rule_id.propagate,
             })
             diff_quantity -= min(procurement_qty, diff_quantity)
             res.append(tmp)
@@ -762,7 +828,13 @@ class purchase_order(osv.osv):
 
     def action_picking_create(self, cr, uid, ids, context=None):
         for order in self.browse(cr, uid, ids):
-            picking_id = self.pool.get('stock.picking').create(cr, uid, {'picking_type_id': order.picking_type_id.id, 'partner_id': order.dest_address_id.id or order.partner_id.id}, context=context)
+            picking_vals = {
+                'picking_type_id': order.picking_type_id.id,
+                'partner_id': order.dest_address_id.id or order.partner_id.id,
+                'date': max([l.date_planned for l in order.order_line]),
+                'origin': order.name
+            }
+            picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
             self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
 
     def picking_done(self, cr, uid, ids, context=None):
@@ -779,20 +851,6 @@ class purchase_order(osv.osv):
         self.message_post(cr, uid, ids, body=_("Products received"), context=context)
         return True
 
-    def copy(self, cr, uid, id, default=None, context=None):
-        if not default:
-            default = {}
-        default.update({
-            'state':'draft',
-            'shipped':False,
-            'invoiced':False,
-            'invoice_ids': [],
-            'origin': '',
-            'partner_ref': '',
-            'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
-        })
-        return super(purchase_order, self).copy(cr, uid, id, default, context)
-
     def do_merge(self, cr, uid, ids, context=None):
         """
         To merge similar type of purchase orders.
@@ -824,14 +882,13 @@ class purchase_order(osv.osv):
                     field_val = field_val.id
                 elif isinstance(field_val, browse_null):
                     field_val = False
-                elif isinstance(field_val, list):
+                elif isinstance(field_val, browse_record_list):
                     field_val = ((6, 0, tuple([v.id for v in field_val])),)
                 list_key.append((field, field_val))
             list_key.sort()
             return tuple(list_key)
 
-        if context is None:
-            context = {}
+        context = dict(context or {})
 
         # Compute what the new orders should contain
         new_orders = {}
@@ -892,7 +949,7 @@ class purchase_order(osv.osv):
             # make triggers pointing to the old orders point to the new order
             for old_id in old_ids:
                 self.redirect_workflow(cr, uid, [(old_id, neworder_id)])
-                self.signal_purchase_cancel(cr, uid, [old_id])
+                self.signal_workflow(cr, uid, [old_id], 'purchase_cancel')
 
         return orders_info
 
@@ -929,15 +986,18 @@ class purchase_order_line(osv.osv):
         'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
         'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
-        'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
+        'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')],
+                                  'Status', required=True, readonly=True, copy=False,
                                   help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
                                        \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
                                        \n* The \'Done\' status is set automatically when purchase order is set as done. \
                                        \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
-        'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
-        '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"),
+        'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel',
+                                          'order_line_id', 'invoice_id', 'Invoice Lines',
+                                          readonly=True, copy=False),
+        'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
+        '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="datetime"),
         'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'),
     }
     _defaults = {
@@ -950,13 +1010,10 @@ class purchase_order_line(osv.osv):
     _name = 'purchase.order.line'
     _description = 'Purchase Order Line'
 
-    def copy_data(self, cr, uid, id, default=None, context=None):
-        if not default:
-            default = {}
-        default.update({'state':'draft', 'move_ids':[], 'invoiced':0, 'invoice_lines':[], 'procurement_ids': False})
-        return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
-
     def unlink(self, cr, uid, ids, context=None):
+        for line in self.browse(cr, uid, ids, context=context):
+            if line.state not in ['draft', 'cancel']:
+                raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
         procurement_obj = self.pool.get('procurement.order')
         procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
         if procurement_ids_to_cancel:
@@ -985,13 +1042,13 @@ class purchase_order_line(osv.osv):
 
            :param browse_record | False supplier_info: product.supplierinfo, used to
                determine delivery delay (if False, default delay = 0)
-           :param str date_order_str: date of order, as a string in
-               DEFAULT_SERVER_DATE_FORMAT
+           :param str date_order_str: date of order field, as a string in
+               DEFAULT_SERVER_DATETIME_FORMAT
            :rtype: datetime
            :return: desired Schedule Date for the PO line
         """
         supplier_delay = int(supplier_info.delay) if supplier_info else 0
-        return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
+        return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay)
 
     def action_cancel(self, cr, uid, ids, context=None):
         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
@@ -1060,17 +1117,18 @@ class purchase_order_line(osv.osv):
 
         # - determine product_qty and date_planned based on seller info
         if not date_order:
-            date_order = fields.date.context_today(self,cr,uid,context=context)
+            date_order = fields.datetime.now()
 
 
         supplierinfo = False
+        precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Unit of Measure')
         for supplier in product.seller_ids:
             if partner_id and (supplier.name.id == partner_id):
                 supplierinfo = supplier
                 if supplierinfo.product_uom.id != uom_id:
                     res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
                 min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
-                if (qty or 0.0) < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
+                if float_compare(min_qty , qty, precision_digits=precision) == 1: # If the supplier quantity is greater than entered from user, set minimal.
                     if qty:
                         res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier has a minimal quantity set to %s %s, you should not purchase less.') % (supplierinfo.min_qty, supplierinfo.product_uom.name)}
                     qty = min_qty
@@ -1081,11 +1139,12 @@ class purchase_order_line(osv.osv):
             res['value'].update({'product_qty': qty})
 
         price = price_unit
-        if state not in ('sent','bid'):
+        if price_unit is False or price_unit is None:
             # - determine price_unit and taxes_id
             if pricelist_id:
+                date_order_str = datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
                 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]
+                        product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order_str})[pricelist_id]
             else:
                 price = product.standard_price
 
@@ -1103,11 +1162,12 @@ class purchase_order_line(osv.osv):
         self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
         return True
 
+
 class procurement_rule(osv.osv):
     _inherit = 'procurement.rule'
 
     def _get_action(self, cr, uid, context=None):
-        return [('buy', 'Buy')] + super(procurement_rule, self)._get_action(cr, uid, context=context)
+        return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context)
 
 
 class procurement_order(osv.osv):
@@ -1234,7 +1294,7 @@ class procurement_order(osv.osv):
         product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
         taxes_ids = procurement.product_id.supplier_taxes_id
         taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
-        name = product.partner_ref
+        name = product.display_name
         if product.description_purchase:
             name += '\n' + product.description_purchase
 
@@ -1269,6 +1329,7 @@ class procurement_order(osv.osv):
                 res[procurement.id] = False
             else:
                 schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
+                purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) 
                 line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
                 #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
                 available_draft_po_ids = po_obj.search(cr, uid, [
@@ -1276,6 +1337,10 @@ class procurement_order(osv.osv):
                     ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
                 if available_draft_po_ids:
                     po_id = available_draft_po_ids[0]
+                    po_rec = po_obj.browse(cr, uid, po_id, context=context)
+                    #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late
+                    if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATETIME_FORMAT) > purchase_date:
+                        po_obj.write(cr, uid, [po_id], {'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
                     #look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line
                     available_po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', po_id), ('product_id', '=', line_vals['product_id']), ('product_uom', '=', line_vals['product_uom'])], context=context)
                     if available_po_line_ids:
@@ -1288,8 +1353,7 @@ class procurement_order(osv.osv):
                         po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
                         linked_po_ids.append(procurement.id)
                 else:
-                    purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
-                    name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
+                    name = seq_obj.next_by_code(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
                     po_vals = {
                         'name': name,
                         'origin': procurement.origin,
@@ -1297,6 +1361,7 @@ class procurement_order(osv.osv):
                         'location_id': procurement.location_id.id,
                         'picking_type_id': procurement.rule_id.picking_type_id.id,
                         'pricelist_id': partner.property_product_pricelist_purchase.id,
+                        'currency_id': partner.property_product_pricelist_purchase and partner.property_product_pricelist_purchase.currency_id.id or procurement.company_id.currency_id.id,
                         'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
                         'company_id': procurement.company_id.id,
                         'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
@@ -1325,20 +1390,59 @@ class mail_mail(osv.Model):
         if mail_sent and mail.model == 'purchase.order':
             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])
+                self.pool.get('purchase.order').signal_workflow(cr, uid, [mail.res_id], 'send_rfq')
         return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
 
 
 class product_template(osv.Model):
     _name = 'product.template'
     _inherit = 'product.template'
+    
+    def _get_buy_route(self, cr, uid, context=None):
+        
+        buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
+        if buy_route:
+            return [buy_route]
+        return []
+
+    def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
+        res = dict.fromkeys(ids, 0)
+        for template in self.browse(cr, uid, ids, context=context):
+            res[template.id] = sum([p.purchase_count for p in template.product_variant_ids])
+        return res
+
     _columns = {
         'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
+        'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
     }
+
     _defaults = {
         'purchase_ok': 1,
+        'route_ids': _get_buy_route,
     }
 
+    def action_view_purchases(self, cr, uid, ids, context=None):
+        products = self._get_products(cr, uid, ids, context=context)
+        result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context)
+        result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
+        return result
+
+class product_product(osv.Model):
+    _name = 'product.product'
+    _inherit = 'product.product'
+    
+    def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
+        Purchase = self.pool['purchase.order']
+        return {
+            product_id: Purchase.search_count(cr,uid, [('order_line.product_id', '=', product_id)], context=context) 
+            for product_id in ids
+        }
+
+    _columns = {
+        'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'),
+    }
+
+
 
 class mail_compose_message(osv.Model):
     _inherit = 'mail.compose.message'
@@ -1347,13 +1451,13 @@ class mail_compose_message(osv.Model):
         context = context or {}
         if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
             context = dict(context, mail_post_autofollow=True)
-            self.pool.get('purchase.order').signal_send_rfq(cr, uid, [context['default_res_id']])
+            self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq')
         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
 
 
 class account_invoice(osv.Model):
     """ Override account_invoice to add Chatter messages on the related purchase
-        orders, logging the invoice reception or payment. """
+        orders, logging the invoice receipt or payment. """
     _inherit = 'account.invoice'
 
     def invoice_validate(self, cr, uid, ids, context=None):
@@ -1398,15 +1502,5 @@ class account_invoice_line(osv.Model):
             readonly=True),
     }
 
-class product_product(osv.osv):
-    _inherit = "product.product"
-
-    def _get_buy_route(self, cr, uid, context=None):
-        buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1]
-        return [buy_route]
-
-    _defaults = {
-        'route_ids': _get_buy_route,
-    }
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: