############################################################################## # # Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved. # # $Id: sale.py 1005 2005-07-25 08:41:42Z nicoe $ # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## import time import netsvc from osv import fields,osv,orm import ir from mx import DateTime class sale_shop(osv.osv): _name = "sale.shop" _description = "Sale Shop" _columns = { 'name': fields.char('Shop name',size=64, required=True), 'payment_default_id':fields.many2one('account.account','Default payment account',required=True), 'payment_account_id':fields.many2many('account.account','sale_shop_account','shop_id','account_id','Payment accounts'), 'warehouse_id':fields.many2one('stock.warehouse','Available Warehouse'), 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist'), 'project_id':fields.many2one('account.project', 'Profit/Cost Center'), } sale_shop() def _incoterm_get(self, cr, user): cr.execute('select code, code||\', \'||name from stock_incoterms where active') return cr.fetchall() class sale_order(osv.osv): _name = "sale.order" _description = "Sale Order" def _amount_untaxed(self, cr, uid, ids, prop, unknow_none,unknow_dict): id_set=",".join(map(str,ids)) cr.execute("SELECT s.id,COALESCE(SUM(l.price_unit*l.product_uos_qty),0) AS amount FROM sale_order s LEFT OUTER JOIN sale_order_line l ON (s.id=l.order_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ") res=dict(cr.fetchall()) return res def _amount_tax(self, cr, uid, ids, prop, unknow_none,unknow_dict): res={} for order in self.browse(cr, uid, ids): val=0.0 for line in order.order_line: for tax in line.tax_id: for c in self.pool.get('account.tax').compute(cr, uid, [tax.id], line.price_unit, line.product_uom_qty): val+=c['amount'] res[order.id]=val return res def _amount_total(self, cr, uid, ids, prop, unknow_none,unknow_dict): res = {} untax = self._amount_untaxed(cr, uid, ids, prop, unknow_none,unknow_dict) tax = self._amount_tax(cr, uid, ids, prop, unknow_none,unknow_dict) for id in ids: res[id] = untax.get(id,0) + tax.get(id,0) return res _columns = { 'name': fields.char('Order Description',size=64, required=True), 'shop_id':fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft':[('readonly',False)]}), 'origin': fields.char('Origin', size=64), 'client_order_ref': fields.char('Order Reference',size=64), 'state': fields.selection([ ('draft','Draft'), ('waiting_date','Waiting Schedule'), ('manual','Manual in progress'), ('progress','In progress'), ('shipping_except','Shipping Exception'), ('invoice_except','Invoice Exception'), ('done','Done'), ('cancel','Cancel') ], 'Order State', readonly=True), 'date_order':fields.date('Date Ordered', required=True, readonly=True, states={'draft':[('readonly',False)]}), 'user_id':fields.many2one('res.users', 'Salesman', states={'draft':[('readonly',False)]}, relate=True), 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, relate=True), 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}), 'partner_order_id':fields.many2one('res.partner.address', 'Ordering Address', readonly=True, required=True, states={'draft':[('readonly',False)]}), 'partner_shipping_id':fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft':[('readonly',False)]}), 'incoterm': fields.selection(_incoterm_get, 'Incoterm',size=3), 'picking_policy': fields.selection([('direct','Direct Delivery'),('one','All at once')], 'Picking Policy', required=True ), 'payment_term': fields.selection((('no','No'), ('month','2% 10 Net 30')), 'Payment Term'), 'order_policy': fields.selection([ ('prepaid','Pay before delivery'), ('manual','Shipping & Manual Invoice'), ('postpaid','Invoice after delivery'), ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]}), 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}), 'project_id':fields.many2one('account.project', 'Profit/Cost Center', readonly=True, states={'draft':[('readonly',False)]}), 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}), 'payment_line': fields.one2many('sale.order.payment', 'order_id', 'Order Payments', readonly=True, states={'draft':[('readonly',False)]}), 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoice', readonly=True), 'picking_ids': fields.many2many('stock.picking', 'sale_order_picking_rel', 'order_id','picking_id', 'Picking List', readonly=True), 'shipped':fields.boolean('Delivered', readonly=True), 'invoiced':fields.boolean('Invoiced', readonly=True), 'note': fields.text('Notes'), 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'), 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'), 'amount_total': fields.function(_amount_total, method=True, string='Total'), } _defaults = { 'picking_policy': lambda *a: 'direct', 'date_order': lambda *a: time.strftime('%Y-%m-%d'), 'order_policy': lambda *a: 'manual', 'state': lambda *a: 'draft', 'user_id': lambda x,y,z,context: z, 'name': lambda self,cr,uid,context: self.pool.get('ir.sequence').get(cr, uid, 'sale.order') } # Form filling def onchange_shop_id(self, cr, uid, ids, shop_id): v={} if shop_id: shop=self.pool.get('sale.shop').browse(cr,uid,shop_id) v['project_id']=shop.project_id.id # Que faire si le client a une pricelist a lui ? if shop.pricelist_id.id: v['pricelist_id']=shop.pricelist_id.id v['payment_default_id']=shop.payment_default_id.id return {'value':v} def action_cancel_draft(self, cr, uid, ids, *args): self.write(cr, uid, ids, {'state':'draft'}) wf_service = netsvc.LocalService("workflow") for inv_id in ids: wf_service.trg_create(uid, 'sale.order', inv_id, cr) return True def onchange_partner_id(self, cr, uid, ids, part): if not part: return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False}} addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact']) pricelist=ir.ir_get(cr,uid, 'meta','product.pricelist',[('res.partner',part)])[0][2] return {'value':{'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'pricelist_id': pricelist}} def button_dummy(self, cr, uid, ids, context={}): return True def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done']): res = False factures = {} def make_invoice(order, lines): a=ir.ir_get(cr,uid,'meta','account.receivable',[('res.partner',o.partner_id.id)])[0][2] inv={ 'name': order.name, 'origin': 'SO:'+str(order.id)+':'+order.name, 'reference': "P%dSO%d"%(order.partner_id.id,order.id), 'account_id': a, 'partner_id': order.partner_id.id, 'address_invoice_id': order.partner_invoice_id.id, 'address_contact_id': order.partner_invoice_id.id, 'project_id': order.project_id.id, 'invoice_line': [(6,0,il)], } inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type' : 'customer_invoice'}) return inv_id for o in self.browse(cr,uid,ids): lines = [] for line in o.order_line: if line.state in states: lines.append(line.id) created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines) if created_lines: factures.setdefault(o.partner_id.id, []).append((o, created_lines)) for val in factures.values(): if grouped: res = make_invoice(val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], [])) for o,l in val: self.write(cr, uid, [o.id], {'state' : 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res)) else: for order, il in val: res = make_invoice(order, il) self.write(cr, uid, [order.id], {'state' : 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res)) return res def action_cancel(self, cr, uid, ids, context={}): for r in self.read(cr,uid,ids,['picking_ids']): for pick in r['picking_ids']: wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr) self.write(cr,uid,ids,{'state':'cancel'}) return True def action_wait(self, cr, uid, ids, *args): for r in self.read(cr,uid,ids,['order_policy','invoice_ids','name','amount_untaxed','partner_id','user_id','order_line']): if self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open'): self.pool.get('res.partner.event').create(cr, uid, {'name':'Sale Order: '+r['name'], 'partner_id':r['partner_id'][0], 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':(r['user_id'] and r['user_id'][0]) or uid, 'partner_type':'customer', 'probability': 1.0, 'planned_revenue':r['amount_untaxed']}) if (r['order_policy']=='manual') and (not r['invoice_ids']): self.write(cr,uid,[r['id']],{'state':'manual'}) else: self.write(cr,uid,[r['id']],{'state':'progress'}) self.pool.get('sale.order.line').button_confirm(cr, uid, r['order_line']) def action_ship_create(self, cr, uid, ids, *args): picking_id=False for order in self.browse(cr, uid, ids, context={}): loc_dest_id = ir.ir_get(cr,uid,'meta','stock.lot.customer',[('res.partner',order.partner_id.id)])[0][2] output_id = order.shop_id.warehouse_id.lot_output_id.id picking_id = self.pool.get('stock.picking').create(cr, uid, { 'name':'SO:'+order.name, 'origin':'SO:'+str(order.id)+':'+order.name, 'type':'out', 'state': 'auto', 'move_type': order.picking_policy, 'loc_move_id': loc_dest_id, 'address_id': order.partner_shipping_id.id, }) for line in order.order_line: if line.product_id and line.product_id.product_tmpl_id.type=='product': location_id = order.shop_id.warehouse_id.lot_stock_id.id move_id = self.pool.get('stock.move').create(cr, uid, { 'name':'SO:'+order.name, 'picking_id': picking_id, 'product_id': line.product_id.id, 'date_planned': line.date_planned, 'product_qty': line.product_uom_qty, 'product_uom': line.product_uom.id, 'product_uos_qty': line.product_uos_qty, 'product_uos': line.product_uos.id, 'tracking_id': False, 'product_packaging' : line.product_packaging.id, 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id, 'location_id': location_id, 'location_dest_id': output_id, 'sale_line_id': line.id, 'state': 'waiting', }) proc_id = self.pool.get('mrp.procurement').create(cr, uid, { 'name': 'SO:'+order.name, 'date_planned': line.date_planned, 'product_id': line.product_id.id, 'product_qty': line.product_uom_qty, 'product_uom': line.product_uom.id, 'location_id': order.shop_id.warehouse_id.lot_stock_id.id, 'procure_method': line.type, 'move_id': move_id, }) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr) val = {'picking_ids':[(6,0,[picking_id])]} if order.state=='shipping_except': val['state']='progress' if (order.order_policy=='manual') and order.invoice_ids: val['state']='manual' self.write(cr, uid, [order.id], val) return picking_id def action_ship_end(self, cr, uid, ids, context={}): for order in self.browse(cr, uid, ids): val={'shipped':True} if order.state=='shipping_except': if (order.order_policy=='manual') and not order.invoice_ids: val['state']='manual' else: val['state'] = 'progress' self.write(cr, uid, [order.id], val) return True def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'): invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed']) for inv in invs: part=inv['partner_id'] and inv['partner_id'][0] pr = inv['amount_untaxed'] or 0.0 partnertype = 'customer' eventtype = 'sale' self.pool.get('res.partner.event').create(cr, uid, {'name':'Order: '+name, 'som':False, 'description':'Order '+str(inv['id']), 'document':'', 'partner_id':part, 'date':time.strftime('%Y-%m-%d'), 'canal_id':False, 'user_id':uid, 'partner_type':partnertype, 'probability':1.0, 'planned_revenue':pr, 'planned_cost':0.0, 'type':eventtype}) def has_stockable_products(self,cr, uid, ids, *args): for order in self.browse(cr, uid, ids): for order_line in order.order_line: if order_line.product_id and order_line.product_id.product_tmpl_id.type=='product': return True return False sale_order() class sale_order_line(osv.osv): def _amount_line(self, cr, uid, ids, field_name, arg, context): res = {} for line in self.browse(cr, uid, ids): if line.product_uos.id: res[line.id] = line.price_unit * line.product_uos_qty else: res[line.id] = line.price_unit * line.product_uom_qty return res def _number_packages(self, cr, uid, ids, field_name, arg, context): res = {} for line in self.browse(cr, uid, ids): res[line.id] = int(line.product_uom_qty / line.product_packaging.qty) return res def _get_1st_packaging(self, cr, uid, context={}): cr.execute('select id from product_packaging order by id asc limit 1') res = cr.fetchone() if not res: return False return res[0] _name = 'sale.order.line' _description = 'Sale Order line' _columns = { 'order_id': fields.many2one('sale.order', 'Order Ref', required=True), 'name': fields.char('Ref', size=64, required=True), 'sequence': fields.integer('Sequence'), 'date_planned': fields.date('Date Promised', required=True), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True), 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines'), 'invoiced': fields.boolean('Invoiced'), 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'), 'price_unit': fields.float('Unit Price', required=True), 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'), 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'), 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True), 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'), 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'), 'product_uom_qty': fields.float('Quantity (UOM)', digits=(16,2), required=True), 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True), 'product_uos_qty' : fields.float('Quantity (UOS)'), 'product_uos' : fields.many2one('product.uom', 'Product UOS'), 'product_packaging' : fields.many2one('product.packaging', 'Packaging used'), 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves'), 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'), 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number packages'), 'notes': fields.text('Notes'), 'th_weight' : fields.float('Weight'), 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done')], 'State', required=True, readonly=True), 'price_unit_customer': fields.float('Customer Unit Price'), } _order = 'sequence' _defaults = { 'date_planned': lambda *a: time.strftime('%Y-%m-%d'), 'product_uom_qty': lambda *a: 1, 'sequence': lambda *a: 10, 'invoiced': lambda *a: 0, 'state': lambda *a: 'draft', 'type': lambda *a: 'make_to_stock', 'product_packaging': _get_1st_packaging, } def invoice_line_create(self, cr, uid, ids, context={}): create_ids = [] for line in self.browse(cr, uid, ids, context): if not line.invoiced: place = [ ('product.product',line.product_id.id) ] if line.product_id and line.product_id.categ_id: place.append( ('product.category', line.product_id.categ_id.id) ) a=ir.ir_get(cr,uid,'meta','account.income',place)[0][2] inv_id = self.pool.get('account.invoice.line').create(cr, uid, { 'name':line.name, 'account_id':a, 'price_unit':line.price_unit, 'quantity':line.product_uos_qty or line.product_uom_qty, 'invoice_line_tax_id':[(6,0,[x.id for x in line.tax_id])] }) cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%d,%d)', (line.id, inv_id)) self.write(cr, uid, [line.id], {'invoiced':True}) create_ids.append(inv_id) return create_ids def button_confirm(self, cr, uid, ids, context={}): return self.write(cr, uid, ids, {'state':'confirmed'}) def button_done(self, cr, uid, ids, context={}): return self.write(cr, uid, ids, {'state':'done'}) def uom_change(self, cr, uid, ids, product_uom, product_uom_qty=0, product_id=None): if not product_id: return {'value': {'product_uos': False, 'product_uos_qty': 0, 'weight' : 0}, 'domain':{}} res = self.pool.get('product.product').read(cr, uid, [product_id], ['uom_id', 'uos_id', 'uos_coeff', 'weight'])[0] q = self.pool.get('product.uom')._compute_qty(cr, uid, product_uom, product_uom_qty, res['uom_id'][0]) return {'value' : {'product_uos' : res['uos_id'], 'product_uos_qty' : q * res['uos_coeff'], 'weight' : q * res['weight']}} def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None): if not product_id: return {'value': {'product_uom': False, 'product_uom_qty': 0}, 'domain':{}} res = self.pool.get('product.product').read(cr, uid, [product_id], ['uom_id', 'uos_id', 'uos_coeff', 'weight'])[0] value = { 'product_uom' : res['uom_id'], } try: value.update({ 'product_uom_qty' : product_uos_qty / res['uos_coeff'], 'weight' : product_uos_qty / res['uos_coeff'] * res['weight'] }) except ZeroDivisionError: pass return {'value' : value} def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name=''): if not product: return {'value': {'price_unit': 0.0, 'notes':'', 'weight' : 0}, 'domain':{'product_uom':[]}} if not pricelist: raise osv.except_osv('No Pricelist !', 'You have to select a pricelist in the sale form !\nPlease set one before choosing a product.') price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], product, qty or 1.0, 'list')[pricelist] if not price: raise osv.except_osv('Product not sellable !', 'You can not sell this product !\nYou have to change either the product, the quantity or the pricelist.') res = self.pool.get('product.product').read(cr, uid, [product])[0] dt = (DateTime.now() + DateTime.RelativeDateTime(days=res['sale_delay'] or 0.0)).strftime('%Y-%m-%d') result = {'price_unit': price, 'name':res['name'], 'tax_id':res['taxes_id'], 'type':res['procure_method'], 'date_planned':dt, 'notes':res['description_sale']} if res['code'] and not name: result['name'] = res['code'] domain = {} if not uom and not uos: result['product_uom'] = res['uom_id'] and res['uom_id'][0] if result['product_uom']: result['product_uos'] = res['uos_id'] result['product_uos_qty'] = qty * res['uos_coeff'] result['weight'] = qty * res['weight'] res2 = self.pool.get('product.uom').read(cr, uid, [result['product_uom']], ['category_id']) if res2 and res2[0]['category_id']: domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]} elif uom: default_uom = res['uom_id'] and res['uom_id'][0] q = self.pool.get('product.uom')._compute_qty(cr, uid, uom, qty, default_uom) result['product_uos'] = res['uos_id'] result['product_uos_qty'] = q * res['uos_coeff'] result['weight'] = q * res['weight'] elif uos: result['product_uom'] = res['uom_id'] and res['uom_id'][0] result['product_uom_qty'] = qty_uos / res['uos_coeff'] result['weight'] = result['product_uom_qty'] * res['weight'] return {'value':result, 'domain':domain} sale_order_line() class sale_payment_line(osv.osv): _name = 'sale.order.payment' _description = 'Sale Order payment' _columns = { 'order_id': fields.many2one('sale.order', 'Order Ref'), 'name': fields.char('Description', size=64, required=True), 'account_id': fields.many2one('account.account', 'Account'), 'amount': fields.float('Amount', required=True), } _defaults = { } sale_payment_line()