Bugfix in purchase copy
[odoo/odoo.git] / addons / purchase / purchase.py
1 ##############################################################################
2 #
3 # Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsability of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # garantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 #
26 ##############################################################################
27
28 from osv import fields
29 from osv import osv
30 import time
31 import netsvc
32
33 import ir
34 from mx import DateTime
35 import pooler
36
37 #
38 # Model definition
39 #
40 class purchase_order(osv.osv):
41         def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
42                 res = {}
43                 for order in self.browse(cr, uid, ids):
44                         res[order.id] = 0
45                         for oline in order.order_line:
46                                 res[order.id] += oline.price_unit * oline.product_qty
47                 return res
48
49         def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
50                 id_set = ",".join(map(str, ids))
51                 cr.execute("SELECT s.id,COALESCE(SUM(l.price_unit*l.product_qty),0)::decimal(16,2) AS amount FROM purchase_order s LEFT OUTER JOIN purchase_order_line l ON (s.id=l.order_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
52                 res = dict(cr.fetchall())
53                 return res
54
55         def _amount_tax(self, cr, uid, ids, field_name, arg, context):
56                 res = {}
57                 for order in self.browse(cr, uid, ids):
58                         val = 0.0
59                         for line in order.order_line:
60                                 for tax in line.taxes_id:
61                                         for c in self.pool.get('account.tax').compute(cr, uid, [tax.id], line.price_unit, line.product_qty, order.partner_address_id.id):
62                                                 val+=c['amount']
63                         res[order.id]=round(val,2)
64                 return res
65
66         def _amount_total(self, cr, uid, ids, field_name, arg, context):
67                 res = {}
68                 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context) 
69                 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
70                 for id in ids:
71                         res[id] = untax.get(id, 0.0) + tax.get(id, 0.0)
72                 return res
73
74         _columns = {
75                 'name': fields.char('Order Description', size=64, required=True, select=True),
76                 'origin': fields.char('Origin', size=64),
77                 'ref': fields.char('Order Reference', size=64),
78                 'partner_ref': fields.char('Partner Reference', size=64),
79                 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
80                 'date_approve':fields.date('Date Approved'),
81                 'partner_id':fields.many2one('res.partner', 'Partner', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True, relate=True),
82                 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
83
84                 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
85                 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}, relate=True),
86                 'location_id': fields.many2one('stock.location', 'Delivery destination', required=True),
87                 'project_id':fields.many2one('account.analytic.account', 'Analytic Account', states={'posted':[('readonly',True)]}),
88
89                 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
90
91                 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Confirmed'), ('approved', 'Approved'),('except_ship', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Order State', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
92                 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order State', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
93                 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
94                 'notes': fields.text('Notes'),
95                 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
96                 'picking_ids': fields.one2many('stock.picking', 'purchase_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this purchase"),
97                 'shipped':fields.boolean('Received', readonly=True, select=True),
98                 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
99                 'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing method', required=True),
100
101                 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
102                 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
103                 'amount_total': fields.function(_amount_total, method=True, string='Total'),
104         }
105         _defaults = {
106                 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
107                 'state': lambda *a: 'draft',
108                 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
109                 'shipped': lambda *a: 0,
110                 'invoice_method': lambda *a: 'order',
111                 'invoiced': lambda *a: 0
112         }
113         _name = "purchase.order"
114         _description = "Purchase order"
115
116         def button_dummy(self, cr, uid, ids, context={}):
117                 return True
118
119         def onchange_dest_address_id(self, cr, uid, ids, adr_id):
120                 if not adr_id:
121                         return {}
122                 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
123                 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer[0]
124                 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
125
126         def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
127                 if not warehouse_id:
128                         return {}
129                 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
130                 return {'value':{'location_id': res, 'dest_address_id': False}}
131
132         def onchange_partner_id(self, cr, uid, ids, part):
133                 if not part:
134                         return {'value':{'partner_address_id': False}}
135                 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
136                 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase[0]
137                 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
138
139         def wkf_approve_order(self, cr, uid, ids):
140                 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
141                 return True
142
143         def wkf_confirm_order(self, cr, uid, ids, context={}):
144                 for po in self.browse(cr, uid, ids):
145                         if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
146                                 self.pool.get('res.partner.event').create(cr, uid, {'name':'Purchase Order: '+po.name, 'partner_id':po.partner_id.id, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':uid, 'partner_type':'retailer', 'probability': 1.0, 'planned_cost':po.amount_untaxed})
147                 current_name = self.name_get(cr, uid, ids)[0][1]
148                 for id in ids:
149                         self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
150                 return True
151         
152         def wkf_warn_buyer(self, cr, uid, ids):
153                 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
154                 request = pooler.get_pool(cr.dbname).get('res.request')
155                 for po in self.browse(cr, uid, ids):
156                         managers = []
157                         for oline in po.order_line:
158                                 manager = oline.product_id.product_manager
159                                 if manager and not (manager.id in managers):
160                                         managers.append(manager.id)
161                         for manager_id in managers:
162                                 request.create(cr, uid, 
163                                           {'name' : "Purchase amount over the limit",
164                                            'act_from' : uid,
165                                            'act_to' : manager_id,
166                                            'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
167                                            'ref_partner_id': po.partner_id.id,
168                                            'ref_doc1': 'purchase.order,%d' % (po.id,),
169                                            })
170
171         def action_invoice_create(self, cr, uid, ids, *args):
172                 res = False
173                 for o in self.browse(cr, uid, ids):
174                         il = []
175                         for ol in o.order_line:
176
177                                 if ol.product_id:
178                                         a = ol.product_id.product_tmpl_id.property_account_expense
179                                         if not a:
180                                                 a = ol.product_id.categ_id.property_account_expense_categ[0]
181                                         else:
182                                                 a = a[0]
183                                 else:
184                                         a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
185                                 il.append((0, False, {
186                                         'name': ol.name,
187                                         'account_id': a,
188                                         'price_unit': ol.price_unit or 0.0,
189                                         'quantity': ol.product_qty,
190                                         'product_id': ol.product_id.id or False,
191                                         'uos_id': ol.product_uom.id or False,
192                                         'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])]
193                                 }))
194
195                         a = o.partner_id.property_account_payable[0]
196                         inv = {
197                                 'name': o.name,
198                                 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
199                                 'account_id': a,
200                                 'type': 'in_invoice',
201                                 'partner_id': o.partner_id.id,
202                                 'currency_id': o.pricelist_id.currency_id.id,
203                                 'project_id': o.project_id.id,
204                                 'address_invoice_id': o.partner_address_id.id,
205                                 'address_contact_id': o.partner_address_id.id,
206                                 'origin': o.name,
207                                 'invoice_line': il,
208                         }
209                         inv_id = self.pool.get('account.invoice').create(cr, uid, inv)
210
211                         self.write(cr, uid, [o.id], {'invoice_id': inv_id})
212                         res = inv_id
213                 return res
214
215         def has_stockable_product(self,cr, uid, ids, *args):
216                 for order in self.browse(cr, uid, ids):
217                         for order_line in order.order_line:
218                                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
219                                         return True
220                 return False
221
222         def action_picking_create(self,cr, uid, ids, *args):
223                 picking_id = False
224                 for order in self.browse(cr, uid, ids):
225                         loc_id = order.partner_id.property_stock_supplier[0]
226                         istate = 'none'
227                         if order.invoice_method=='picking':
228                                 istate = '2binvoiced'
229                         picking_id = self.pool.get('stock.picking').create(cr, uid, {
230                                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
231                                 'type': 'in',
232                                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
233                                 'invoice_state': istate,
234                                 'purchase_id': order.id,
235                         })
236                         for order_line in order.order_line:
237                                 if not order_line.product_id:
238                                         continue
239                                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
240                                         dest = order.location_id.id
241                                         self.pool.get('stock.move').create(cr, uid, {
242                                                 'name': 'PO:'+order_line.name,
243                                                 'product_id': order_line.product_id.id,
244                                                 'product_qty': order_line.product_qty,
245                                                 'product_uos_qty': order_line.product_qty,
246                                                 'product_uom': order_line.product_uom.id,
247                                                 'product_uos': order_line.product_uom.id,
248                                                 'date_planned': order_line.date_planned,
249                                                 'location_id': loc_id,
250                                                 'location_dest_id': dest,
251                                                 'picking_id': picking_id,
252                                                 'move_dest_id': order_line.move_dest_id.id,
253                                                 'state': 'assigned',
254                                                 'purchase_line_id': order_line.id,
255                                         })
256                                         if order_line.move_dest_id:
257                                                 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
258                         wf_service = netsvc.LocalService("workflow")
259                         wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
260                 return picking_id
261         def copy(self, cr, uid, id, default=None,context={}):
262                 if not default:
263                         default = {}
264                 default.update({
265                         'state':'draft',
266                         'shipped':False,
267                         'invoiced':False,
268                         'invoice_id':False,
269                         'picking_ids':[],
270                         'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
271                 })
272                 return super(purchase_order, self).copy(cr, uid, id, default, context)
273
274 purchase_order()
275
276 class purchase_order_line(osv.osv):
277         def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
278                 res = {}
279                 for line in self.browse(cr, uid, ids):
280                         res[line.id] = line.price_unit * line.product_qty
281                 return res
282         
283         _columns = {
284                 'name': fields.char('Description', size=64, required=True),
285                 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
286                 'date_planned': fields.date('Date Promised', required=True),
287                 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
288                 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
289                 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True, relate=True),
290                 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
291                 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
292                 'price_unit': fields.float('Unit Price', required=True),
293                 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
294                 'notes': fields.text('Notes'),
295                 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True)
296         }
297         _defaults = {
298                 'product_qty': lambda *a: 1.0
299         }
300         _table = 'purchase_order_line'
301         _name = 'purchase.order.line'
302         _description = 'Purchase Order line'
303         def copy(self, cr, uid, id, default=None,context={}):
304                 if not default:
305                         default = {}
306                 default.update({'state':'draft', 'move_id':False})
307                 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
308
309         def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom, partner_id):
310                 if not pricelist:
311                         raise osv.except_osv('No Pricelist !', 'You have to select a pricelist in the sale form !\n Please set one before choosing a product.')
312                 if not product:
313                         return {'value': {'price_unit': 0.0, 'name':'','notes':''}, 'domain':{'product_uom':[]}}
314                 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
315                 prod = self.pool.get('product.product').read(cr, uid, [product], ['taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
316                 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d')
317                 prod_name = self.pool.get('product.product').name_get(cr, uid, [product])[0][1]
318                 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['taxes_id'], 'date_planned': dt,'notes':prod['description_purchase']}}
319                 domain = {}
320                 if not uom:
321                         res['value']['product_uom'] = prod['uom_po_id'][0]
322                         if res['value']['product_uom']:
323                                 res2 = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['category_id'])
324                                 if res2 and res2[0]['category_id']:
325                                         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
326                 res['domain'] = domain
327                 return res
328 purchase_order_line()