~tempo-openerp/+junk/loewert-report-name

« back to all changes in this revision

Viewing changes to addons/purchase/purchase.py

  • Committer: jbe at tempo-consulting
  • Date: 2013-08-21 08:48:11 UTC
  • Revision ID: jbe@tempo-consulting.fr-20130821084811-913uo4l7b5ayxq8m
[NEW] Création de la branche trunk Loewert

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
4
#    OpenERP, Open Source Management Solution
 
5
#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
 
6
#
 
7
#    This program is free software: you can redistribute it and/or modify
 
8
#    it under the terms of the GNU Affero General Public License as
 
9
#    published by the Free Software Foundation, either version 3 of the
 
10
#    License, or (at your option) any later version.
 
11
#
 
12
#    This program is distributed in the hope that it will be useful,
 
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
#    GNU Affero General Public License for more details.
 
16
#
 
17
#    You should have received a copy of the GNU Affero General Public License
 
18
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
19
#
 
20
##############################################################################
 
21
 
 
22
import time
 
23
import pytz
 
24
from openerp import SUPERUSER_ID
 
25
from datetime import datetime
 
26
from dateutil.relativedelta import relativedelta
 
27
 
 
28
from openerp.osv import fields, osv
 
29
from openerp import netsvc
 
30
from openerp import pooler
 
31
from openerp.tools.translate import _
 
32
import openerp.addons.decimal_precision as dp
 
33
from openerp.osv.orm import browse_record, browse_null
 
34
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
 
35
 
 
36
class purchase_order(osv.osv):
 
37
 
 
38
    def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
 
39
        res = {}
 
40
        cur_obj=self.pool.get('res.currency')
 
41
        for order in self.browse(cr, uid, ids, context=context):
 
42
            res[order.id] = {
 
43
                'amount_untaxed': 0.0,
 
44
                'amount_tax': 0.0,
 
45
                'amount_total': 0.0,
 
46
            }
 
47
            val = val1 = 0.0
 
48
            cur = order.pricelist_id.currency_id
 
49
            for line in order.order_line:
 
50
               val1 += line.price_subtotal
 
51
               for c in self.pool.get('account.tax').compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, order.partner_id)['taxes']:
 
52
                    val += c.get('amount', 0.0)
 
53
            res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
 
54
            res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
 
55
            res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
 
56
        return res
 
57
 
 
58
    def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None):
 
59
        if not value: return False
 
60
        if type(ids)!=type([]):
 
61
            ids=[ids]
 
62
        for po in self.browse(cr, uid, ids, context=context):
 
63
            if po.order_line:
 
64
                cr.execute("""update purchase_order_line set
 
65
                        date_planned=%s
 
66
                    where
 
67
                        order_id=%s and
 
68
                        (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
 
69
            cr.execute("""update purchase_order set
 
70
                    minimum_planned_date=%s where id=%s""", (value, po.id))
 
71
        return True
 
72
 
 
73
    def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None):
 
74
        res={}
 
75
        purchase_obj=self.browse(cr, uid, ids, context=context)
 
76
        for purchase in purchase_obj:
 
77
            res[purchase.id] = False
 
78
            if purchase.order_line:
 
79
                min_date=purchase.order_line[0].date_planned
 
80
                for line in purchase.order_line:
 
81
                    if line.date_planned < min_date:
 
82
                        min_date=line.date_planned
 
83
                res[purchase.id]=min_date
 
84
        return res
 
85
 
 
86
 
 
87
    def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
 
88
        res = {}
 
89
        for purchase in self.browse(cursor, user, ids, context=context):
 
90
            tot = 0.0
 
91
            for invoice in purchase.invoice_ids:
 
92
                if invoice.state not in ('draft','cancel'):
 
93
                    tot += invoice.amount_untaxed
 
94
            if purchase.amount_untaxed:
 
95
                res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
 
96
            else:
 
97
                res[purchase.id] = 0.0
 
98
        return res
 
99
 
 
100
    def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
 
101
        if not ids: return {}
 
102
        res = {}
 
103
        for id in ids:
 
104
            res[id] = [0.0,0.0]
 
105
        cr.execute('''SELECT
 
106
                p.purchase_id,sum(m.product_qty), m.state
 
107
            FROM
 
108
                stock_move m
 
109
            LEFT JOIN
 
110
                stock_picking p on (p.id=m.picking_id)
 
111
            WHERE
 
112
                p.purchase_id IN %s GROUP BY m.state, p.purchase_id''',(tuple(ids),))
 
113
        for oid,nbr,state in cr.fetchall():
 
114
            if state=='cancel':
 
115
                continue
 
116
            if state=='done':
 
117
                res[oid][0] += nbr or 0.0
 
118
                res[oid][1] += nbr or 0.0
 
119
            else:
 
120
                res[oid][1] += nbr or 0.0
 
121
        for r in res:
 
122
            if not res[r][1]:
 
123
                res[r] = 0.0
 
124
            else:
 
125
                res[r] = 100.0 * res[r][0] / res[r][1]
 
126
        return res
 
127
 
 
128
    def _get_order(self, cr, uid, ids, context=None):
 
129
        result = {}
 
130
        for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
 
131
            result[line.order_id.id] = True
 
132
        return result.keys()
 
133
 
 
134
    def _invoiced(self, cursor, user, ids, name, arg, context=None):
 
135
        res = {}
 
136
        for purchase in self.browse(cursor, user, ids, context=context):
 
137
            invoiced = False
 
138
            if purchase.invoiced_rate == 100.00:
 
139
                invoiced = True
 
140
            res[purchase.id] = invoiced
 
141
        return res
 
142
    
 
143
    def _get_journal(self, cr, uid, context=None):
 
144
        if context is None:
 
145
            context = {}
 
146
        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
 
147
        company_id = context.get('company_id', user.company_id.id)
 
148
        journal_obj = self.pool.get('account.journal')
 
149
        res = journal_obj.search(cr, uid, [('type', '=', 'purchase'),
 
150
                                            ('company_id', '=', company_id)],
 
151
                                                limit=1)
 
152
        return res and res[0] or False  
 
153
 
 
154
    STATE_SELECTION = [
 
155
        ('draft', 'Draft PO'),
 
156
        ('sent', 'RFQ Sent'),
 
157
        ('confirmed', 'Waiting Approval'),
 
158
        ('approved', 'Purchase Order'),
 
159
        ('except_picking', 'Shipping Exception'),
 
160
        ('except_invoice', 'Invoice Exception'),
 
161
        ('done', 'Done'),
 
162
        ('cancel', 'Cancelled')
 
163
    ]
 
164
    _track = {
 
165
        'state': {
 
166
            'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirmed',
 
167
            'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'approved',
 
168
        },
 
169
    }
 
170
    _columns = {
 
171
        '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."),
 
172
        'origin': fields.char('Source Document', size=64,
 
173
            help="Reference of the document that generated this purchase order request; a sales order or an internal procurement request."
 
174
        ),
 
175
        'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64,
 
176
            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."),
 
177
        '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."),
 
178
        'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"),
 
179
        'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
 
180
            change_default=True, track_visibility='always'),
 
181
        'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)',
 
182
            states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]},
 
183
            help="Put an address if you want to deliver directly from the supplier to the customer. " \
 
184
                "Otherwise, keep empty to deliver to your own company."
 
185
        ),
 
186
        'warehouse_id': fields.many2one('stock.warehouse', 'Destination Warehouse'),
 
187
        'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ),
 
188
        '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."),
 
189
        'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency",readonly=True, required=True),
 
190
        '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),
 
191
        'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
 
192
        'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
 
193
        'notes': fields.text('Terms and Conditions'),
 
194
        'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', 'invoice_id', 'Invoices', help="Invoices generated for a purchase order"),
 
195
        'picking_ids': fields.one2many('stock.picking.in', 'purchase_id', 'Picking List', readonly=True, help="This is the list of incoming shipments that have been generated for this purchase order."),
 
196
        'shipped':fields.boolean('Received', readonly=True, select=True, help="It indicates that a picking has been done"),
 
197
        'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'),
 
198
        'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', help="It indicates that an invoice has been paid"),
 
199
        'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
 
200
        '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,
 
201
            readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
 
202
            help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' from where you can selectively create an invoice.\n" \
 
203
                "Based on generated invoice: create a draft invoice you can validate later.\n" \
 
204
                "Bases on incoming shipments: let you create an invoice when receptions are validated."
 
205
        ),
 
206
        '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.",
 
207
            store = {
 
208
                'purchase.order.line': (_get_order, ['date_planned'], 10),
 
209
            }
 
210
        ),
 
211
        'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
 
212
            store={
 
213
                'purchase.order.line': (_get_order, None, 10),
 
214
            }, multi="sums", help="The amount without tax", track_visibility='always'),
 
215
        'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
 
216
            store={
 
217
                'purchase.order.line': (_get_order, None, 10),
 
218
            }, multi="sums", help="The tax amount"),
 
219
        'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
 
220
            store={
 
221
                'purchase.order.line': (_get_order, None, 10),
 
222
            }, multi="sums",help="The total amount"),
 
223
        'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
 
224
        'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'),
 
225
        'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
 
226
        'create_uid':  fields.many2one('res.users', 'Responsible'),
 
227
        'company_id': fields.many2one('res.company','Company',required=True,select=1, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
 
228
        'journal_id': fields.many2one('account.journal', 'Journal'),
 
229
        'num_semaine': fields.char('Semaine de livraison'),
 
230
    }
 
231
    _defaults = {
 
232
        'date_order': fields.date.context_today,
 
233
        'state': 'draft',
 
234
        'name': lambda obj, cr, uid, context: '/',
 
235
        'shipped': 0,
 
236
        'invoice_method': 'order',
 
237
        'invoiced': 0,
 
238
        'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist_purchase.id,
 
239
        'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
 
240
        'journal_id': _get_journal,
 
241
        'num_semaine': lambda *a: time.strftime('%W') + '/' + time.strftime('%Y'),
 
242
    }
 
243
    _sql_constraints = [
 
244
        ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
 
245
    ]
 
246
    _name = "purchase.order"
 
247
    _inherit = ['mail.thread', 'ir.needaction_mixin']
 
248
    _description = "Purchase Order"
 
249
    _order = "name desc"
 
250
 
 
251
    def create(self, cr, uid, vals, context=None):
 
252
        if vals.get('name','/')=='/':
 
253
            vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
 
254
        order =  super(purchase_order, self).create(cr, uid, vals, context=context)
 
255
        return order
 
256
 
 
257
    def unlink(self, cr, uid, ids, context=None):
 
258
        purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
 
259
        unlink_ids = []
 
260
        for s in purchase_orders:
 
261
            if s['state'] in ['draft','cancel']:
 
262
                unlink_ids.append(s['id'])
 
263
            else:
 
264
                raise osv.except_osv(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.'))
 
265
 
 
266
        # automatically sending subflow.delete upon deletion
 
267
        wf_service = netsvc.LocalService("workflow")
 
268
        for id in unlink_ids:
 
269
            wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
 
270
 
 
271
        return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
 
272
 
 
273
    def button_dummy(self, cr, uid, ids, context=None):
 
274
        return True
 
275
 
 
276
    def onchange_pricelist(self, cr, uid, ids, pricelist_id, context=None):
 
277
        if not pricelist_id:
 
278
            return {}
 
279
        return {'value': {'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id}}
 
280
 
 
281
    def onchange_dest_address_id(self, cr, uid, ids, address_id):
 
282
        if not address_id:
 
283
            return {}
 
284
        address = self.pool.get('res.partner')
 
285
        values = {'warehouse_id': False}
 
286
        supplier = address.browse(cr, uid, address_id)
 
287
        if supplier:
 
288
            location_id = supplier.property_stock_customer.id
 
289
            values.update({'location_id': location_id})
 
290
        return {'value':values}
 
291
 
 
292
    def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
 
293
        if not warehouse_id:
 
294
            return {}
 
295
        warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id)
 
296
        return {'value':{'location_id': warehouse.lot_input_id.id, 'dest_address_id': False}}
 
297
 
 
298
    def onchange_partner_id(self, cr, uid, ids, partner_id):
 
299
        partner = self.pool.get('res.partner')
 
300
        if not partner_id:
 
301
            return {'value': {
 
302
                'fiscal_position': False,
 
303
                'payment_term_id': False,
 
304
                }}
 
305
        supplier_address = partner.address_get(cr, uid, [partner_id], ['default'])
 
306
        supplier = partner.browse(cr, uid, partner_id)
 
307
        return {'value': {
 
308
            'pricelist_id': supplier.property_product_pricelist_purchase.id,
 
309
            'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
 
310
            'payment_term_id': supplier.property_supplier_payment_term.id or False,
 
311
            }}
 
312
 
 
313
    def invoice_open(self, cr, uid, ids, context=None):
 
314
        mod_obj = self.pool.get('ir.model.data')
 
315
        act_obj = self.pool.get('ir.actions.act_window')
 
316
 
 
317
        result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree2')
 
318
        id = result and result[1] or False
 
319
        result = act_obj.read(cr, uid, [id], context=context)[0]
 
320
        inv_ids = []
 
321
        for po in self.browse(cr, uid, ids, context=context):
 
322
            inv_ids+= [invoice.id for invoice in po.invoice_ids]
 
323
        if not inv_ids:
 
324
            raise osv.except_osv(_('Error!'), _('Please create Invoices.'))
 
325
         #choose the view_mode accordingly
 
326
        if len(inv_ids)>1:
 
327
            result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
 
328
        else:
 
329
            res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
 
330
            result['views'] = [(res and res[1] or False, 'form')]
 
331
            result['res_id'] = inv_ids and inv_ids[0] or False
 
332
        return result
 
333
 
 
334
    def view_invoice(self, cr, uid, ids, context=None):
 
335
        '''
 
336
        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.
 
337
        '''
 
338
        mod_obj = self.pool.get('ir.model.data')
 
339
        wizard_obj = self.pool.get('purchase.order.line_invoice')
 
340
        #compute the number of invoices to display
 
341
        inv_ids = []
 
342
        for po in self.browse(cr, uid, ids, context=context):
 
343
            if po.invoice_method == 'manual':
 
344
                if not po.invoice_ids:
 
345
                    context.update({'active_ids' :  [line.id for line in po.order_line]})
 
346
                    wizard_obj.makeInvoices(cr, uid, [], context=context)
 
347
 
 
348
        for po in self.browse(cr, uid, ids, context=context):
 
349
            inv_ids+= [invoice.id for invoice in po.invoice_ids]
 
350
        res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
 
351
        res_id = res and res[1] or False
 
352
 
 
353
        return {
 
354
            'name': _('Supplier Invoices'),
 
355
            'view_type': 'form',
 
356
            'view_mode': 'form',
 
357
            'view_id': [res_id],
 
358
            'res_model': 'account.invoice',
 
359
            'context': "{'type':'in_invoice', 'journal_type': 'purchase'}",
 
360
            'type': 'ir.actions.act_window',
 
361
            'nodestroy': True,
 
362
            'target': 'current',
 
363
            'res_id': inv_ids and inv_ids[0] or False,
 
364
        }
 
365
 
 
366
    def view_picking(self, cr, uid, ids, context=None):
 
367
        '''
 
368
        This function returns an action that display existing pîcking orders of given purchase order ids.
 
369
        '''
 
370
        mod_obj = self.pool.get('ir.model.data')
 
371
        pick_ids = []
 
372
        for po in self.browse(cr, uid, ids, context=context):
 
373
            pick_ids += [picking.id for picking in po.picking_ids]
 
374
 
 
375
        action_model, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree4'))
 
376
        action = self.pool.get(action_model).read(cr, uid, action_id, context=context)
 
377
        ctx = eval(action['context'])
 
378
        ctx.update({
 
379
            'search_default_purchase_id': ids[0]
 
380
        })
 
381
        if pick_ids and len(pick_ids) == 1:
 
382
            form_view_ids = [view_id for view_id, view in action['views'] if view == 'form']
 
383
            view_id = form_view_ids and form_view_ids[0] or False
 
384
            action.update({
 
385
                'views': [],
 
386
                'view_mode': 'form',
 
387
                'view_id': view_id,
 
388
                'res_id': pick_ids[0]
 
389
            })
 
390
 
 
391
        action.update({
 
392
            'context': ctx,
 
393
        })
 
394
        return action
 
395
 
 
396
    def wkf_approve_order(self, cr, uid, ids, context=None):
 
397
        self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)})
 
398
        return True
 
399
 
 
400
    def print_confirm(self,cr,uid,ids,context=None):
 
401
        print "Confirmed"
 
402
 
 
403
    def print_double(self,cr,uid,ids,context=None):
 
404
        print "double Approval"
 
405
 
 
406
    def print_router(self,cr,uid,ids,context=None):
 
407
        print "Routed"
 
408
 
 
409
    def wkf_send_rfq(self, cr, uid, ids, context=None):
 
410
        '''
 
411
        This function opens a window to compose an email, with the edi purchase template message loaded by default
 
412
        '''
 
413
        ir_model_data = self.pool.get('ir.model.data')
 
414
        try:
 
415
            template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
 
416
        except ValueError:
 
417
            template_id = False
 
418
        try:
 
419
            compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
 
420
        except ValueError:
 
421
            compose_form_id = False 
 
422
        ctx = dict(context)
 
423
        ctx.update({
 
424
            'default_model': 'purchase.order',
 
425
            'default_res_id': ids[0],
 
426
            'default_use_template': bool(template_id),
 
427
            'default_template_id': template_id,
 
428
            'default_composition_mode': 'comment',
 
429
        })
 
430
        return {
 
431
            'type': 'ir.actions.act_window',
 
432
            'view_type': 'form',
 
433
            'view_mode': 'form',
 
434
            'res_model': 'mail.compose.message',
 
435
            'views': [(compose_form_id, 'form')],
 
436
            'view_id': compose_form_id,
 
437
            'target': 'new',
 
438
            'context': ctx,
 
439
        }
 
440
 
 
441
    def print_quotation(self, cr, uid, ids, context=None):
 
442
        '''
 
443
        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
 
444
        '''
 
445
        assert len(ids) == 1, 'This option should only be used for a single id at a time'
 
446
        wf_service = netsvc.LocalService("workflow")
 
447
        wf_service.trg_validate(uid, 'purchase.order', ids[0], 'send_rfq', cr)
 
448
        datas = {
 
449
                 'model': 'purchase.order',
 
450
                 'ids': ids,
 
451
                 'form': self.read(cr, uid, ids[0], context=context),
 
452
        }
 
453
        return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
 
454
 
 
455
    #TODO: implement messages system
 
456
    def wkf_confirm_order(self, cr, uid, ids, context=None):
 
457
        todo = []
 
458
        for po in self.browse(cr, uid, ids, context=context):
 
459
            if not po.order_line:
 
460
                raise osv.except_osv(_('Error!'),_('You cannot confirm a purchase order without any purchase order line.'))
 
461
            for line in po.order_line:
 
462
                if line.state=='draft':
 
463
                    todo.append(line.id)
 
464
 
 
465
        self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context)
 
466
        for id in ids:
 
467
            self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
 
468
        return True
 
469
 
 
470
    def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
 
471
        """Collects require data from purchase order line that is used to create invoice line
 
472
        for that purchase order line
 
473
        :param account_id: Expense account of the product of PO line if any.
 
474
        :param browse_record order_line: Purchase order line browse record
 
475
        :return: Value for fields of invoice lines.
 
476
        :rtype: dict
 
477
        """
 
478
        return {
 
479
            'name': order_line.name,
 
480
            'account_id': account_id,
 
481
            'price_unit': order_line.price_unit or 0.0,
 
482
            'quantity': order_line.product_qty,
 
483
            'product_id': order_line.product_id.id or False,
 
484
            'uos_id': order_line.product_uom.id or False,
 
485
            'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
 
486
            'account_analytic_id': order_line.account_analytic_id.id or False,
 
487
        }
 
488
 
 
489
    def action_cancel_draft(self, cr, uid, ids, context=None):
 
490
        if not len(ids):
 
491
            return False
 
492
        self.write(cr, uid, ids, {'state':'draft','shipped':0})
 
493
        wf_service = netsvc.LocalService("workflow")
 
494
        for p_id in ids:
 
495
            # Deleting the existing instance of workflow for PO
 
496
            wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
 
497
            wf_service.trg_create(uid, 'purchase.order', p_id, cr)
 
498
        return True
 
499
 
 
500
    def action_invoice_create(self, cr, uid, ids, context=None):
 
501
        """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order.
 
502
        :param ids: list of ids of purchase orders.
 
503
        :return: ID of created invoice.
 
504
        :rtype: int
 
505
        """
 
506
        res = False
 
507
 
 
508
        journal_obj = self.pool.get('account.journal')
 
509
        inv_obj = self.pool.get('account.invoice')
 
510
        inv_line_obj = self.pool.get('account.invoice.line')
 
511
        fiscal_obj = self.pool.get('account.fiscal.position')
 
512
        property_obj = self.pool.get('ir.property')
 
513
 
 
514
        for order in self.browse(cr, uid, ids, context=context):
 
515
            pay_acc_id = order.partner_id.property_account_payable.id
 
516
            journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
 
517
            if not journal_ids:
 
518
                raise osv.except_osv(_('Error!'),
 
519
                    _('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
 
520
 
 
521
            # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
 
522
            inv_lines = []
 
523
            for po_line in order.order_line:
 
524
                if po_line.product_id:
 
525
                    acc_id = po_line.product_id.property_account_expense.id
 
526
                    if not acc_id:
 
527
                        acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
 
528
                    if not acc_id:
 
529
                        raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
 
530
                else:
 
531
                    acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
 
532
                fpos = order.fiscal_position or False
 
533
                acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
 
534
 
 
535
                inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
 
536
                inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
 
537
                inv_lines.append(inv_line_id)
 
538
 
 
539
                po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
 
540
 
 
541
            # get invoice data and create invoice
 
542
            inv_data = {
 
543
                'name': order.partner_ref or order.name,
 
544
                'reference': order.partner_ref or order.name,
 
545
                'account_id': pay_acc_id,
 
546
                'type': 'in_invoice',
 
547
                'partner_id': order.partner_id.id,
 
548
                'currency_id': order.pricelist_id.currency_id.id,
 
549
                'journal_id': len(journal_ids) and journal_ids[0] or False,
 
550
                'invoice_line': [(6, 0, inv_lines)],
 
551
                'origin': order.name,
 
552
                'fiscal_position': order.fiscal_position.id or False,
 
553
                'payment_term': order.payment_term_id.id or False,
 
554
                'company_id': order.company_id.id,
 
555
            }
 
556
            inv_id = inv_obj.create(cr, uid, inv_data, context=context)
 
557
 
 
558
            # compute the invoice
 
559
            inv_obj.button_compute(cr, uid, [inv_id], context=context, set_total=True)
 
560
 
 
561
            # Link this new invoice to related purchase order
 
562
            order.write({'invoice_ids': [(4, inv_id)]}, context=context)
 
563
            res = inv_id
 
564
        return res
 
565
 
 
566
    def invoice_done(self, cr, uid, ids, context=None):
 
567
        self.write(cr, uid, ids, {'state':'approved'}, context=context)
 
568
        return True
 
569
 
 
570
    def has_stockable_product(self, cr, uid, ids, *args):
 
571
        for order in self.browse(cr, uid, ids):
 
572
            for order_line in order.order_line:
 
573
                if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
 
574
                    return True
 
575
        return False
 
576
 
 
577
    def action_cancel(self, cr, uid, ids, context=None):
 
578
        wf_service = netsvc.LocalService("workflow")
 
579
        for purchase in self.browse(cr, uid, ids, context=context):
 
580
            for pick in purchase.picking_ids:
 
581
                if pick.state not in ('draft','cancel'):
 
582
                    raise osv.except_osv(
 
583
                        _('Unable to cancel this purchase order.'),
 
584
                        _('First cancel all receptions related to this purchase order.'))
 
585
            for pick in purchase.picking_ids:
 
586
                wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
 
587
            for inv in purchase.invoice_ids:
 
588
                if inv and inv.state not in ('cancel','draft'):
 
589
                    raise osv.except_osv(
 
590
                        _('Unable to cancel this purchase order.'),
 
591
                        _('You must first cancel all receptions related to this purchase order.'))
 
592
                if inv:
 
593
                    wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
 
594
        self.write(cr,uid,ids,{'state':'cancel'})
 
595
 
 
596
        for (id, name) in self.name_get(cr, uid, ids):
 
597
            wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
 
598
        return True
 
599
 
 
600
    def date_to_datetime(self, cr, uid, userdate, context=None):
 
601
        """ Convert date values expressed in user's timezone to
 
602
        server-side UTC timestamp, assuming a default arbitrary
 
603
        time of 12:00 AM - because a time is needed.
 
604
    
 
605
        :param str userdate: date string in in user time zone
 
606
        :return: UTC datetime string for server-side use
 
607
        """
 
608
        # TODO: move to fields.datetime in server after 7.0
 
609
        user_date = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT)
 
610
        if context and context.get('tz'):
 
611
            tz_name = context['tz']
 
612
        else:
 
613
            tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
 
614
        if tz_name:
 
615
            utc = pytz.timezone('UTC')
 
616
            context_tz = pytz.timezone(tz_name)
 
617
            user_datetime = user_date + relativedelta(hours=12.0)
 
618
            local_timestamp = context_tz.localize(user_datetime, is_dst=False)
 
619
            user_datetime = local_timestamp.astimezone(utc)
 
620
            return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 
621
        return user_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 
622
 
 
623
    def _prepare_order_picking(self, cr, uid, order, context=None):
 
624
        return {
 
625
            'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
 
626
            'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
 
627
            'date': self.date_to_datetime(cr, uid, order.date_order, context),
 
628
            'partner_id': order.dest_address_id.id or order.partner_id.id,
 
629
            'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
 
630
            'type': 'in',
 
631
            'partner_id': order.dest_address_id.id or order.partner_id.id,
 
632
            'purchase_id': order.id,
 
633
            'company_id': order.company_id.id,
 
634
            'move_lines' : [],
 
635
        }
 
636
 
 
637
    def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
 
638
        return {
 
639
            'name': order_line.name or '',
 
640
            'product_id': order_line.product_id.id,
 
641
            'product_qty': order_line.product_qty,
 
642
            'product_uos_qty': order_line.product_qty,
 
643
            'product_uom': order_line.product_uom.id,
 
644
            'product_uos': order_line.product_uom.id,
 
645
            'date': self.date_to_datetime(cr, uid, order.date_order, context),
 
646
            'date_expected': self.date_to_datetime(cr, uid, order_line.date_planned, context),
 
647
            'location_id': order.partner_id.property_stock_supplier.id,
 
648
            'location_dest_id': order.location_id.id,
 
649
            'picking_id': picking_id,
 
650
            'partner_id': order.dest_address_id.id or order.partner_id.id,
 
651
            'move_dest_id': order_line.move_dest_id.id,
 
652
            'state': 'draft',
 
653
            'type':'in',
 
654
            'purchase_line_id': order_line.id,
 
655
            'company_id': order.company_id.id,
 
656
            'price_unit': order_line.price_unit
 
657
        }
 
658
 
 
659
    def _create_pickings(self, cr, uid, order, order_lines, picking_id=False, context=None):
 
660
        """Creates pickings and appropriate stock moves for given order lines, then
 
661
        confirms the moves, makes them available, and confirms the picking.
 
662
 
 
663
        If ``picking_id`` is provided, the stock moves will be added to it, otherwise
 
664
        a standard outgoing picking will be created to wrap the stock moves, as returned
 
665
        by :meth:`~._prepare_order_picking`.
 
666
 
 
667
        Modules that wish to customize the procurements or partition the stock moves over
 
668
        multiple stock pickings may override this method and call ``super()`` with
 
669
        different subsets of ``order_lines`` and/or preset ``picking_id`` values.
 
670
 
 
671
        :param browse_record order: purchase order to which the order lines belong
 
672
        :param list(browse_record) order_lines: purchase order line records for which picking
 
673
                                                and moves should be created.
 
674
        :param int picking_id: optional ID of a stock picking to which the created stock moves
 
675
                               will be added. A new picking will be created if omitted.
 
676
        :return: list of IDs of pickings used/created for the given order lines (usually just one)
 
677
        """
 
678
        if not picking_id:
 
679
            picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
 
680
        todo_moves = []
 
681
        stock_move = self.pool.get('stock.move')
 
682
        wf_service = netsvc.LocalService("workflow")
 
683
        for order_line in order_lines:
 
684
            if not order_line.product_id:
 
685
                continue
 
686
            if order_line.product_id.type in ('product', 'consu'):
 
687
                move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
 
688
                if order_line.move_dest_id:
 
689
                    order_line.move_dest_id.write({'location_id': order.location_id.id})
 
690
                todo_moves.append(move)
 
691
        stock_move.action_confirm(cr, uid, todo_moves)
 
692
        stock_move.force_assign(cr, uid, todo_moves)
 
693
        wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
 
694
        return [picking_id]
 
695
 
 
696
    def action_picking_create(self, cr, uid, ids, context=None):
 
697
        picking_ids = []
 
698
        for order in self.browse(cr, uid, ids):
 
699
            picking_ids.extend(self._create_pickings(cr, uid, order, order.order_line, None, context=context))
 
700
 
 
701
        # Must return one unique picking ID: the one to connect in the subflow of the purchase order.
 
702
        # In case of multiple (split) pickings, we should return the ID of the critical one, i.e. the
 
703
        # one that should trigger the advancement of the purchase workflow.
 
704
        # By default we will consider the first one as most important, but this behavior can be overridden.
 
705
        return picking_ids[0] if picking_ids else False
 
706
 
 
707
    def picking_done(self, cr, uid, ids, context=None):
 
708
        self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
 
709
        return True
 
710
 
 
711
    def copy(self, cr, uid, id, default=None, context=None):
 
712
        if not default:
 
713
            default = {}
 
714
        default.update({
 
715
            'state':'draft',
 
716
            'shipped':False,
 
717
            'invoiced':False,
 
718
            'invoice_ids': [],
 
719
            'picking_ids': [],
 
720
            'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
 
721
        })
 
722
        return super(purchase_order, self).copy(cr, uid, id, default, context)
 
723
 
 
724
    def do_merge(self, cr, uid, ids, context=None):
 
725
        """
 
726
        To merge similar type of purchase orders.
 
727
        Orders will only be merged if:
 
728
        * Purchase Orders are in draft
 
729
        * Purchase Orders belong to the same partner
 
730
        * Purchase Orders are have same stock location, same pricelist
 
731
        Lines will only be merged if:
 
732
        * Order lines are exactly the same except for the quantity and unit
 
733
 
 
734
         @param self: The object pointer.
 
735
         @param cr: A database cursor
 
736
         @param uid: ID of the user currently logged in
 
737
         @param ids: the ID or list of IDs
 
738
         @param context: A standard dictionary
 
739
 
 
740
         @return: new purchase order id
 
741
 
 
742
        """
 
743
        #TOFIX: merged order line should be unlink
 
744
        wf_service = netsvc.LocalService("workflow")
 
745
        def make_key(br, fields):
 
746
            list_key = []
 
747
            for field in fields:
 
748
                field_val = getattr(br, field)
 
749
                if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
 
750
                    if not field_val:
 
751
                        field_val = False
 
752
                if isinstance(field_val, browse_record):
 
753
                    field_val = field_val.id
 
754
                elif isinstance(field_val, browse_null):
 
755
                    field_val = False
 
756
                elif isinstance(field_val, list):
 
757
                    field_val = ((6, 0, tuple([v.id for v in field_val])),)
 
758
                list_key.append((field, field_val))
 
759
            list_key.sort()
 
760
            return tuple(list_key)
 
761
 
 
762
        # Compute what the new orders should contain
 
763
 
 
764
        new_orders = {}
 
765
 
 
766
        for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']:
 
767
            order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
 
768
            new_order = new_orders.setdefault(order_key, ({}, []))
 
769
            new_order[1].append(porder.id)
 
770
            order_infos = new_order[0]
 
771
            if not order_infos:
 
772
                order_infos.update({
 
773
                    'origin': porder.origin,
 
774
                    'date_order': porder.date_order,
 
775
                    'partner_id': porder.partner_id.id,
 
776
                    'dest_address_id': porder.dest_address_id.id,
 
777
                    'warehouse_id': porder.warehouse_id.id,
 
778
                    'location_id': porder.location_id.id,
 
779
                    'pricelist_id': porder.pricelist_id.id,
 
780
                    'state': 'draft',
 
781
                    'order_line': {},
 
782
                    'notes': '%s' % (porder.notes or '',),
 
783
                    'fiscal_position': porder.fiscal_position and porder.fiscal_position.id or False,
 
784
                })
 
785
            else:
 
786
                if porder.date_order < order_infos['date_order']:
 
787
                    order_infos['date_order'] = porder.date_order
 
788
                if porder.notes:
 
789
                    order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
 
790
                if porder.origin:
 
791
                    order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
 
792
 
 
793
            for order_line in porder.order_line:
 
794
                line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'product_id', 'move_dest_id', 'account_analytic_id'))
 
795
                o_line = order_infos['order_line'].setdefault(line_key, {})
 
796
                if o_line:
 
797
                    # merge the line with an existing line
 
798
                    o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
 
799
                else:
 
800
                    # append a new "standalone" line
 
801
                    for field in ('product_qty', 'product_uom'):
 
802
                        field_val = getattr(order_line, field)
 
803
                        if isinstance(field_val, browse_record):
 
804
                            field_val = field_val.id
 
805
                        o_line[field] = field_val
 
806
                    o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
 
807
 
 
808
 
 
809
 
 
810
        allorders = []
 
811
        orders_info = {}
 
812
        for order_key, (order_data, old_ids) in new_orders.iteritems():
 
813
            # skip merges with only one order
 
814
            if len(old_ids) < 2:
 
815
                allorders += (old_ids or [])
 
816
                continue
 
817
 
 
818
            # cleanup order line data
 
819
            for key, value in order_data['order_line'].iteritems():
 
820
                del value['uom_factor']
 
821
                value.update(dict(key))
 
822
            order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
 
823
 
 
824
            # create the new order
 
825
            neworder_id = self.create(cr, uid, order_data)
 
826
            orders_info.update({neworder_id: old_ids})
 
827
            allorders.append(neworder_id)
 
828
 
 
829
            # make triggers pointing to the old orders point to the new order
 
830
            for old_id in old_ids:
 
831
                wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
 
832
                wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
 
833
        return orders_info
 
834
 
 
835
 
 
836
class purchase_order_line(osv.osv):
 
837
    def _amount_line(self, cr, uid, ids, prop, arg, context=None):
 
838
        res = {}
 
839
        cur_obj=self.pool.get('res.currency')
 
840
        tax_obj = self.pool.get('account.tax')
 
841
        for line in self.browse(cr, uid, ids, context=context):
 
842
            taxes = tax_obj.compute_all(cr, uid, line.taxes_id, line.price_unit, line.product_qty, line.product_id, line.order_id.partner_id)
 
843
            cur = line.order_id.pricelist_id.currency_id
 
844
            res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
 
845
        return res
 
846
 
 
847
    _columns = {
 
848
        'name': fields.text('Description', required=True),
 
849
        'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
 
850
        'date_planned': fields.date('Scheduled Date', required=True, select=True),
 
851
        'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
 
852
        'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
 
853
        'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
 
854
        'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
 
855
        'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
 
856
        'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
 
857
        'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
 
858
        'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
 
859
        'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
 
860
        'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
 
861
        'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
 
862
                                  help=' * The \'Draft\' status is set automatically when purchase order in draft status. \
 
863
                                       \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \
 
864
                                       \n* The \'Done\' status is set automatically when purchase order is set as done. \
 
865
                                       \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'),
 
866
        'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
 
867
        'invoiced': fields.boolean('Invoiced', readonly=True),
 
868
        'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
 
869
        'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
 
870
 
 
871
    }
 
872
    _defaults = {
 
873
        'product_qty': lambda *a: 1.0,
 
874
        'state': lambda *args: 'draft',
 
875
        'invoiced': lambda *a: 0,
 
876
    }
 
877
    _table = 'purchase_order_line'
 
878
    _name = 'purchase.order.line'
 
879
    _description = 'Purchase Order Line'
 
880
 
 
881
    def copy_data(self, cr, uid, id, default=None, context=None):
 
882
        if not default:
 
883
            default = {}
 
884
        default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
 
885
        return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
 
886
 
 
887
    def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
 
888
            partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
 
889
            name=False, price_unit=False, context=None):
 
890
        """
 
891
        onchange handler of product_uom.
 
892
        """
 
893
        if not uom_id:
 
894
            return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
 
895
        return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
 
896
            partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
 
897
            name=name, price_unit=price_unit, context=context)
 
898
 
 
899
    def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None):
 
900
        """Return the datetime value to use as Schedule Date (``date_planned``) for
 
901
           PO Lines that correspond to the given product.supplierinfo,
 
902
           when ordered at `date_order_str`.
 
903
 
 
904
           :param browse_record | False supplier_info: product.supplierinfo, used to
 
905
               determine delivery delay (if False, default delay = 0)
 
906
           :param str date_order_str: date of order, as a string in
 
907
               DEFAULT_SERVER_DATE_FORMAT
 
908
           :rtype: datetime
 
909
           :return: desired Schedule Date for the PO line
 
910
        """
 
911
        supplier_delay = int(supplier_info.delay) if supplier_info else 0
 
912
        return datetime.strptime(date_order_str, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=supplier_delay)
 
913
 
 
914
    def _check_product_uom_group(self, cr, uid, context=None):
 
915
        group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom')
 
916
        res = [user for user in group_uom.users if user.id == uid]
 
917
        return len(res) and True or False
 
918
 
 
919
 
 
920
    def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
 
921
            partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
 
922
            name=False, price_unit=False, context=None):
 
923
        """
 
924
        onchange handler of product_id.
 
925
        """
 
926
        if context is None:
 
927
            context = {}
 
928
 
 
929
        res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
 
930
        if not product_id:
 
931
            return res
 
932
 
 
933
        product_product = self.pool.get('product.product')
 
934
        product_uom = self.pool.get('product.uom')
 
935
        res_partner = self.pool.get('res.partner')
 
936
        product_supplierinfo = self.pool.get('product.supplierinfo')
 
937
        product_pricelist = self.pool.get('product.pricelist')
 
938
        account_fiscal_position = self.pool.get('account.fiscal.position')
 
939
        account_tax = self.pool.get('account.tax')
 
940
 
 
941
        # - check for the presence of partner_id and pricelist_id
 
942
        #if not partner_id:
 
943
        #    raise osv.except_osv(_('No Partner!'), _('Select a partner in purchase order to choose a product.'))
 
944
        #if not pricelist_id:
 
945
        #    raise osv.except_osv(_('No Pricelist !'), _('Select a price list in the purchase order form before choosing a product.'))
 
946
 
 
947
        # - determine name and notes based on product in partner lang.
 
948
        context_partner = context.copy()
 
949
        if partner_id:
 
950
            lang = res_partner.browse(cr, uid, partner_id).lang
 
951
            context_partner.update( {'lang': lang, 'partner_id': partner_id} )
 
952
        product = product_product.browse(cr, uid, product_id, context=context_partner)
 
953
        #call name_get() with partner in the context to eventually match name and description in the seller_ids field
 
954
        dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
 
955
        if product.description_purchase:
 
956
            name += '\n' + product.description_purchase
 
957
        res['value'].update({'name': name})
 
958
 
 
959
        # - set a domain on product_uom
 
960
        res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]}
 
961
 
 
962
        # - check that uom and product uom belong to the same category
 
963
        product_uom_po_id = product.uom_po_id.id
 
964
        if not uom_id:
 
965
            uom_id = product_uom_po_id
 
966
 
 
967
        if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
 
968
            if self._check_product_uom_group(cr, uid, context=context):
 
969
                res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
 
970
            uom_id = product_uom_po_id
 
971
 
 
972
        res['value'].update({'product_uom': uom_id})
 
973
 
 
974
        # - determine product_qty and date_planned based on seller info
 
975
        if not date_order:
 
976
            date_order = fields.date.context_today(self,cr,uid,context=context)
 
977
 
 
978
 
 
979
        supplierinfo = False
 
980
        for supplier in product.seller_ids:
 
981
            if partner_id and (supplier.name.id == partner_id):
 
982
                supplierinfo = supplier
 
983
                if supplierinfo.product_uom.id != uom_id:
 
984
                    res['warning'] = {'title': _('Warning!'), 'message': _('The selected supplier only sells this product by %s') % supplierinfo.product_uom.name }
 
985
                min_qty = product_uom._compute_qty(cr, uid, supplierinfo.product_uom.id, supplierinfo.min_qty, to_uom_id=uom_id)
 
986
                if (qty or 0.0) < min_qty: # If the supplier quantity is greater than entered from user, set minimal.
 
987
                    if qty:
 
988
                        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)}
 
989
                    qty = min_qty
 
990
        dt = self._get_date_planned(cr, uid, supplierinfo, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 
991
        qty = qty or 1.0
 
992
        res['value'].update({'date_planned': date_planned or dt})
 
993
        if qty:
 
994
            res['value'].update({'product_qty': qty})
 
995
 
 
996
        # - determine price_unit and taxes_id
 
997
        if pricelist_id:
 
998
            price = product_pricelist.price_get(cr, uid, [pricelist_id],
 
999
                    product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id]
 
1000
        else:
 
1001
            price = product.standard_price
 
1002
 
 
1003
        taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id))
 
1004
        fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False
 
1005
        taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes)
 
1006
        res['value'].update({'price_unit': price, 'taxes_id': taxes_ids})
 
1007
 
 
1008
        return res
 
1009
 
 
1010
    product_id_change = onchange_product_id
 
1011
    product_uom_change = onchange_product_uom
 
1012
 
 
1013
    def action_confirm(self, cr, uid, ids, context=None):
 
1014
        self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
 
1015
        return True
 
1016
 
 
1017
purchase_order_line()
 
1018
 
 
1019
class procurement_order(osv.osv):
 
1020
    _inherit = 'procurement.order'
 
1021
    _columns = {
 
1022
        'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
 
1023
    }
 
1024
 
 
1025
    def check_buy(self, cr, uid, ids, context=None):
 
1026
        ''' return True if the supply method of the mto product is 'buy'
 
1027
        '''
 
1028
        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
 
1029
        for procurement in self.browse(cr, uid, ids, context=context):
 
1030
            if procurement.product_id.supply_method <> 'buy':
 
1031
                return False
 
1032
        return True
 
1033
 
 
1034
    def check_supplier_info(self, cr, uid, ids, context=None):
 
1035
        partner_obj = self.pool.get('res.partner')
 
1036
        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
 
1037
        for procurement in self.browse(cr, uid, ids, context=context):
 
1038
            if not procurement.product_id.seller_ids:
 
1039
                message = _('No supplier defined for this product !')
 
1040
                self.message_post(cr, uid, [procurement.id], body=message)
 
1041
                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 
1042
                return False
 
1043
            partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
 
1044
 
 
1045
            if not partner:
 
1046
                message = _('No default supplier defined for this product')
 
1047
                self.message_post(cr, uid, [procurement.id], body=message)
 
1048
                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 
1049
                return False
 
1050
            if user.company_id and user.company_id.partner_id:
 
1051
                if partner.id == user.company_id.partner_id.id:
 
1052
                    raise osv.except_osv(_('Configuration Error!'), _('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name))
 
1053
 
 
1054
            address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
 
1055
            if not address_id:
 
1056
                message = _('No address defined for the supplier')
 
1057
                self.message_post(cr, uid, [procurement.id], body=message)
 
1058
                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 
1059
                return False
 
1060
        return True
 
1061
 
 
1062
 
 
1063
    def action_po_assign(self, cr, uid, ids, context=None):
 
1064
        """ This is action which call from workflow to assign purchase order to procurements
 
1065
        @return: True
 
1066
        """
 
1067
        res = self.make_po(cr, uid, ids, context=context)
 
1068
        res = res.values()
 
1069
        return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value
 
1070
 
 
1071
    def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None):
 
1072
        """Create the purchase order from the procurement, using
 
1073
           the provided field values, after adding the given purchase
 
1074
           order line in the purchase order.
 
1075
 
 
1076
           :params procurement: the procurement object generating the purchase order
 
1077
           :params dict po_vals: field values for the new purchase order (the
 
1078
                                 ``order_line`` field will be overwritten with one
 
1079
                                 single line, as passed in ``line_vals``).
 
1080
           :params dict line_vals: field values of the single purchase order line that
 
1081
                                   the purchase order will contain.
 
1082
           :return: id of the newly created purchase order
 
1083
           :rtype: int
 
1084
        """
 
1085
        po_vals.update({'order_line': [(0,0,line_vals)]})
 
1086
        return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context)
 
1087
 
 
1088
    def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
 
1089
        """Return the datetime value to use as Schedule Date (``date_planned``) for the
 
1090
           Purchase Order Lines created to satisfy the given procurement.
 
1091
 
 
1092
           :param browse_record procurement: the procurement for which a PO will be created.
 
1093
           :param browse_report company: the company to which the new PO will belong to.
 
1094
           :rtype: datetime
 
1095
           :return: the desired Schedule Date for the PO lines
 
1096
        """
 
1097
        procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
 
1098
        schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead))
 
1099
        return schedule_date
 
1100
 
 
1101
    def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
 
1102
        """Return the datetime value to use as Order Date (``date_order``) for the
 
1103
           Purchase Order created to satisfy the given procurement.
 
1104
 
 
1105
           :param browse_record procurement: the procurement for which a PO will be created.
 
1106
           :param browse_report company: the company to which the new PO will belong to.
 
1107
           :param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
 
1108
           :rtype: datetime
 
1109
           :return: the desired Order Date for the PO
 
1110
        """
 
1111
        seller_delay = int(procurement.product_id.seller_delay)
 
1112
        return schedule_date - relativedelta(days=seller_delay)
 
1113
 
 
1114
    def make_po(self, cr, uid, ids, context=None):
 
1115
        """ Make purchase order from procurement
 
1116
        @return: New created Purchase Orders procurement wise
 
1117
        """
 
1118
        res = {}
 
1119
        if context is None:
 
1120
            context = {}
 
1121
        company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
 
1122
        partner_obj = self.pool.get('res.partner')
 
1123
        uom_obj = self.pool.get('product.uom')
 
1124
        pricelist_obj = self.pool.get('product.pricelist')
 
1125
        prod_obj = self.pool.get('product.product')
 
1126
        acc_pos_obj = self.pool.get('account.fiscal.position')
 
1127
        seq_obj = self.pool.get('ir.sequence')
 
1128
        warehouse_obj = self.pool.get('stock.warehouse')
 
1129
        for procurement in self.browse(cr, uid, ids, context=context):
 
1130
            res_id = procurement.move_id.id
 
1131
            partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
 
1132
            seller_qty = procurement.product_id.seller_qty
 
1133
            partner_id = partner.id
 
1134
            address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
 
1135
            pricelist_id = partner.property_product_pricelist_purchase.id
 
1136
            warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
 
1137
            uom_id = procurement.product_id.uom_po_id.id
 
1138
 
 
1139
            qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
 
1140
            if seller_qty:
 
1141
                qty = max(qty,seller_qty)
 
1142
 
 
1143
            price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
 
1144
 
 
1145
            schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
 
1146
            purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
 
1147
 
 
1148
            #Passing partner_id to context for purchase order line integrity of Line name
 
1149
            new_context = context.copy()
 
1150
            new_context.update({'lang': partner.lang, 'partner_id': partner_id})
 
1151
 
 
1152
            product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
 
1153
            taxes_ids = procurement.product_id.supplier_taxes_id
 
1154
            taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
 
1155
 
 
1156
            name = product.partner_ref
 
1157
            if product.description_purchase:
 
1158
                name += '\n'+ product.description_purchase
 
1159
            line_vals = {
 
1160
                'name': name,
 
1161
                'product_qty': qty,
 
1162
                'product_id': procurement.product_id.id,
 
1163
                'product_uom': uom_id,
 
1164
                'price_unit': price or 0.0,
 
1165
                'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
 
1166
                'move_dest_id': res_id,
 
1167
                'taxes_id': [(6,0,taxes)],
 
1168
            }
 
1169
            name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
 
1170
            po_vals = {
 
1171
                'name': name,
 
1172
                'origin': procurement.origin,
 
1173
                'partner_id': partner_id,
 
1174
                'location_id': procurement.location_id.id,
 
1175
                'warehouse_id': warehouse_id and warehouse_id[0] or False,
 
1176
                'pricelist_id': pricelist_id,
 
1177
                'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
 
1178
                'company_id': procurement.company_id.id,
 
1179
                'fiscal_position': partner.property_account_position and partner.property_account_position.id or False,
 
1180
                'payment_term_id': partner.property_supplier_payment_term.id or False,
 
1181
            }
 
1182
            res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=new_context)
 
1183
            self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]})
 
1184
        self.message_post(cr, uid, ids, body=_("Draft Purchase Order created"), context=context)
 
1185
        return res
 
1186
 
 
1187
    def _product_virtual_get(self, cr, uid, order_point):
 
1188
        procurement = order_point.procurement_id
 
1189
        if procurement and procurement.state != 'exception' and procurement.purchase_id and procurement.purchase_id.state in ('draft', 'confirmed'):
 
1190
            return None
 
1191
        return super(procurement_order, self)._product_virtual_get(cr, uid, order_point)
 
1192
 
 
1193
 
 
1194
class mail_mail(osv.Model):
 
1195
    _name = 'mail.mail'
 
1196
    _inherit = 'mail.mail'
 
1197
 
 
1198
    def _postprocess_sent_message(self, cr, uid, mail, context=None):
 
1199
        if mail.model == 'purchase.order':
 
1200
            wf_service = netsvc.LocalService("workflow")
 
1201
            wf_service.trg_validate(uid, 'purchase.order', mail.res_id, 'send_rfq', cr)
 
1202
        return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
 
1203
 
 
1204
 
 
1205
class product_template(osv.Model):
 
1206
    _name = 'product.template'
 
1207
    _inherit = 'product.template'
 
1208
    _columns = {
 
1209
        'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."),
 
1210
    }
 
1211
    _defaults = {
 
1212
        'purchase_ok': 1,
 
1213
    }
 
1214
 
 
1215
 
 
1216
class mail_compose_message(osv.Model):
 
1217
    _inherit = 'mail.compose.message'
 
1218
 
 
1219
    def send_mail(self, cr, uid, ids, context=None):
 
1220
        context = context or {}
 
1221
        if context.get('default_model') == 'purchase.order' and context.get('default_res_id'):
 
1222
            context = dict(context, mail_post_autofollow=True)
 
1223
            wf_service = netsvc.LocalService("workflow")
 
1224
            wf_service.trg_validate(uid, 'purchase.order', context['default_res_id'], 'send_rfq', cr)
 
1225
        return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
 
1226
 
 
1227
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: