X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fsale%2Fsale.py;h=364ca36b1c2ecd5f6499d4fcfdcb15d5adb5e47b;hb=16ce262c33d331a5d1c61caf9a042942125168ce;hp=cc99e99303a29324753754d7099ae01a9162fdcc;hpb=a0e65cecc82b47af588a0d4f4f02c4ecbe7281a0;p=odoo%2Fodoo.git diff --git a/addons/sale/sale.py b/addons/sale/sale.py index cc99e99..364ca36 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -22,12 +22,12 @@ from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import time -import pooler -from osv import fields, osv -from tools.translate import _ -from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare -import decimal_precision as dp -import netsvc +from openerp import pooler +from openerp.osv import fields, osv +from openerp.tools.translate import _ +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare +import openerp.addons.decimal_precision as dp +from openerp import netsvc class sale_shop(osv.osv): _name = "sale.shop" @@ -49,6 +49,12 @@ class sale_order(osv.osv): _name = "sale.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Sales Order" + _track = { + 'state': { + 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['manual', 'progress'], + 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['sent'] + }, + } def onchange_shop_id(self, cr, uid, ids, shop_id, context=None): v = {} @@ -64,9 +70,11 @@ class sale_order(osv.osv): if not default: default = {} default.update({ + 'date_order': fields.date.context_today(self, cr, uid, context=context), 'state': 'draft', 'invoice_ids': [], 'date_confirm': False, + 'client_order_ref': '', 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'), }) return super(sale_order, self).copy(cr, uid, id, default, context=context) @@ -172,6 +180,13 @@ class sale_order(osv.osv): result[line.order_id.id] = True return result.keys() + def _get_default_shop(self, cr, uid, context=None): + company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id + shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context) + if not shop_ids: + raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!')) + return shop_ids[0] + _columns = { 'name': fields.char('Order Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), @@ -183,56 +198,56 @@ class sale_order(osv.osv): ('sent', 'Quotation Sent'), ('cancel', 'Cancelled'), ('waiting_date', 'Waiting Schedule'), - ('progress', 'Sale Order'), + ('progress', 'Sales Order'), ('manual', 'Sale to Invoice'), ('invoice_except', 'Invoice Exception'), ('done', 'Done'), - ], 'Status', readonly=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True), + ], 'Status', readonly=True, track_visibility='onchange', + help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True), 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."), 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."), - 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), - 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True), + 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'), + 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'), 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."), 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."), 'order_policy': fields.selection([ ('manual', 'On Demand'), ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, - help="""This field controls how invoice and delivery operations are synchronized. - - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""), + help="""This field controls how invoice and delivery operations are synchronized."""), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."), - 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", readonly=True, required=True), - 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."), + 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True), + 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."), 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."), - 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'), + 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'), 'invoiced': fields.function(_invoiced, string='Paid', fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."), 'invoice_exists': fields.function(_invoice_exists, string='Invoiced', - fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."), + fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."), 'note': fields.text('Terms and conditions'), - 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount', - store = { + 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount', + store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, - multi='sums', help="The amount without tax."), - 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes', - store = { + multi='sums', help="The amount without tax.", track_visibility='always'), + 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes', + store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, multi='sums', help="The tax amount."), - 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total', - store = { + 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total', + store={ 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), }, multi='sums', help="The total amount."), - 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}), + 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}), 'payment_term': fields.many2one('account.payment.term', 'Payment Term'), 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'), 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True) @@ -242,15 +257,16 @@ class sale_order(osv.osv): 'order_policy': 'manual', 'state': 'draft', 'user_id': lambda obj, cr, uid, context: uid, - 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'), + 'name': lambda obj, cr, uid, context: '/', 'invoice_quantity': 'order', + 'shop_id': _get_default_shop, 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'], 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'], } _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), ] - _order = 'name desc' + _order = 'date_order desc, id desc' # Form filling def unlink(self, cr, uid, ids, context=None): @@ -260,11 +276,28 @@ class sale_order(osv.osv): if s['state'] in ['draft', 'cancel']: unlink_ids.append(s['id']) else: - raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !')) + raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!')) return osv.osv.unlink(self, cr, uid, unlink_ids, context=context) + def copy_quotation(self, cr, uid, ids, context=None): + id = self.copy(cr, uid, ids[0], context=None) + view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form') + view_id = view_ref and view_ref[1] or False, + return { + 'type': 'ir.actions.act_window', + 'name': _('Sales Order'), + 'res_model': 'sale.order', + 'res_id': id, + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': view_id, + 'target': 'current', + 'nodestroy': True, + } + def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None): + context = context or {} if not pricelist_id: return {} value = { @@ -278,12 +311,12 @@ class sale_order(osv.osv): } return {'warning': warning, 'value': value} - def onchange_partner_id(self, cr, uid, ids, part): + def onchange_partner_id(self, cr, uid, ids, part, context=None): if not part: return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}} - addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact']) - part = self.pool.get('res.partner').browse(cr, uid, part) + part = self.pool.get('res.partner').browse(cr, uid, part, context=context) + addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact']) pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False payment_term = part.property_payment_term and part.property_payment_term.id or False fiscal_position = part.property_account_position and part.property_account_position.id or False @@ -300,10 +333,9 @@ class sale_order(osv.osv): return {'value': val} def create(self, cr, uid, vals, context=None): - order = super(sale_order, self).create(cr, uid, vals, context=context) - if order: - self.create_send_note(cr, uid, [order], context=context) - return order + if vals.get('name','/')=='/': + vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/' + return super(sale_order, self).create(cr, uid, vals, context=context) def button_dummy(self, cr, uid, ids, context=None): return True @@ -315,7 +347,7 @@ class sale_order(osv.osv): def _prepare_invoice(self, cr, uid, order, lines, context=None): """Prepare the dict of values to create the new invoice for a - sale order. This method may be overridden to implement custom + sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). @@ -338,7 +370,7 @@ class sale_order(osv.osv): 'type': 'out_invoice', 'reference': order.client_order_ref or order.name, 'account_id': order.partner_id.property_account_receivable.id, - 'partner_id': order.partner_id.id, + 'partner_id': order.partner_invoice_id.id, 'journal_id': journal_ids[0], 'invoice_line': [(6, 0, lines)], 'currency_id': order.pricelist_id.currency_id.id, @@ -380,7 +412,7 @@ class sale_order(osv.osv): def print_quotation(self, cr, uid, ids, context=None): ''' - This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow + This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow ''' assert len(ids) == 1, 'This option should only be used for a single id at a time' wf_service = netsvc.LocalService("workflow") @@ -393,13 +425,13 @@ class sale_order(osv.osv): return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True} def manual_invoice(self, cr, uid, ids, context=None): - """ create invoices for the given sale orders (ids), and open the form + """ create invoices for the given sales orders (ids), and open the form view of one of the newly created invoices """ mod_obj = self.pool.get('ir.model.data') wf_service = netsvc.LocalService("workflow") - # create invoices through the sale orders' workflow + # create invoices through the sales orders' workflow inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) for id in ids: wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr) @@ -425,7 +457,7 @@ class sale_order(osv.osv): def action_view_invoice(self, cr, uid, ids, context=None): ''' - This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show. + 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. ''' mod_obj = self.pool.get('ir.model.data') act_obj = self.pool.get('ir.actions.act_window') @@ -452,7 +484,7 @@ class sale_order(osv.osv): return False return True - def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None): + def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None): if states is None: states = ['confirmed', 'done', 'exception'] res = False @@ -465,8 +497,8 @@ class sale_order(osv.osv): context = {} # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the # last day of the last month as invoice date - if date_inv: - context['date_inv'] = date_inv + if date_invoice: + context['date_invoice'] = date_invoice for o in self.browse(cr, uid, ids, context=context): currency_id = o.pricelist_id.currency_id.id if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id): @@ -483,7 +515,7 @@ class sale_order(osv.osv): lines.append(line.id) created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines) if created_lines: - invoices.setdefault(o.partner_id.id, []).append((o, created_lines)) + invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines)) if not invoices: for o in self.browse(cr, uid, ids, context=context): for i in o.invoice_ids: @@ -497,6 +529,9 @@ class sale_order(osv.osv): invoice_ref += o.name + '|' self.write(cr, uid, [o.id], {'state': 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res)) + #remove last '|' in invoice_ref + if len(invoice_ref) >= 1: + invoice_ref = invoice_ref[:-1] invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref}) else: for order, il in val: @@ -504,60 +539,19 @@ class sale_order(osv.osv): invoice_ids.append(res) self.write(cr, uid, [order.id], {'state': 'progress'}) cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res)) - if res: - self.invoice_send_note(cr, uid, ids, res, context) return res def action_invoice_cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - for sale in self.browse(cr, uid, ids, context=context): - for line in sale.order_line: - # - # Check if the line is invoiced (has asociated invoice - # lines from non-cancelled invoices). - # - invoiced = False - for iline in line.invoice_lines: - if iline.invoice_id and iline.invoice_id.state != 'cancel': - invoiced = True - break - # Update the line (only when needed) - if line.invoiced != invoiced: - self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context) - self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context) + self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context) return True def action_invoice_end(self, cr, uid, ids, context=None): - for order in self.browse(cr, uid, ids, context=context): - # - # Update the sale order lines state (and invoiced flag). - # - for line in order.order_line: - vals = {} - # - # Check if the line is invoiced (has asociated invoice - # lines from non-cancelled invoices). - # - invoiced = False - for iline in line.invoice_lines: - if iline.invoice_id and iline.invoice_id.state != 'cancel': - invoiced = True - break - if line.invoiced != invoiced: - vals['invoiced'] = invoiced - # If the line was in exception state, now it gets confirmed. + for this in self.browse(cr, uid, ids, context=context): + for line in this.order_line: if line.state == 'exception': - vals['state'] = 'confirmed' - # Update the line (only when needed). - if vals: - self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context) - # - # Update the sales order state. - # - if order.state == 'invoice_except': - self.write(cr, uid, [order.id], {'state': 'progress'}, context=context) - self.invoice_paid_send_note(cr, uid, [order.id], context=context) + line.write({'state': 'confirmed'}) + if this.state == 'invoice_except': + this.write({'state': 'progress'}) return True def action_cancel(self, cr, uid, ids, context=None): @@ -576,7 +570,6 @@ class sale_order(osv.osv): wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr) sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], {'state': 'cancel'}) - self.cancel_send_note(cr, uid, [sale.id], context=None) self.write(cr, uid, ids, {'state': 'cancel'}) return True @@ -585,7 +578,7 @@ class sale_order(osv.osv): wf_service = netsvc.LocalService('workflow') wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr) - # redisplay the record as a sale order + # redisplay the record as a sales order view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form') view_id = view_ref and view_ref[1] or False, return { @@ -601,16 +594,16 @@ class sale_order(osv.osv): } def action_wait(self, cr, uid, ids, context=None): + context = context or {} for o in self.browse(cr, uid, ids): if not o.order_line: - raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.')) + raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.')) noprod = self.test_no_product(cr, uid, o, context) if (o.order_policy == 'manual') or noprod: self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) else: self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line]) - self.confirm_send_note(cr, uid, ids, context) return True def action_quotation_send(self, cr, uid, ids, context=None): @@ -618,67 +611,39 @@ class sale_order(osv.osv): This function opens a window to compose an email, with the edi sale template message loaded by default ''' assert len(ids) == 1, 'This option should only be used for a single id at a time.' - mod_obj = self.pool.get('ir.model.data') - template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale') - template_id = template and template[1] or False - res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form') - res_id = res and res[1] or False + ir_model_data = self.pool.get('ir.model.data') + try: + template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1] + except ValueError: + template_id = False + try: + compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] + except ValueError: + compose_form_id = False ctx = dict(context) ctx.update({ 'default_model': 'sale.order', 'default_res_id': ids[0], - 'default_use_template': True, + 'default_use_template': bool(template_id), 'default_template_id': template_id, + 'default_composition_mode': 'comment', 'mark_so_as_sent': True }) return { + 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', - 'views': [(res_id, 'form')], - 'view_id': res_id, - 'type': 'ir.actions.act_window', + 'views': [(compose_form_id, 'form')], + 'view_id': compose_form_id, 'target': 'new', 'context': ctx, - 'nodestroy': True, } def action_done(self, cr, uid, ids, context=None): - self.done_send_note(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state': 'done'}, context=context) - # ------------------------------------------------ - # OpenChatter methods and notifications - # ------------------------------------------------ - - def needaction_domain_get(self, cr, uid, ids, context=None): - return [('state', '=', 'draft'), ('user_id','=',uid)] - - def create_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Quotation for %s created.") % (obj.partner_id.name), context=context) - - def confirm_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Quotation for %s converted to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context) - - def cancel_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Sale Order for %s cancelled.") % (obj.partner_id.name), context=context) - - def done_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Sale Order for %s set to Done") % (obj.partner_id.name), context=context) - def invoice_paid_send_note(self, cr, uid, ids, context=None): - self.message_post(cr, uid, ids, body=_("Invoice paid."), context=context) - - def invoice_send_note(self, cr, uid, ids, invoice_id, context=None): - for order in self.browse(cr, uid, ids, context=context): - for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id): - self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s waiting for validation.") % (invoice.amount_total, invoice.currency_id.symbol), context=context) - -sale_order() # TODO add a field price_unit_uos # - update it on change product and unit price @@ -706,21 +671,38 @@ class sale_order_line(osv.osv): except Exception, ex: return False + def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None): + res = dict.fromkeys(ids, False) + for this in self.browse(cr, uid, ids, context=context): + res[this.id] = this.invoice_lines and \ + all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) + return res + + def _order_lines_from_invoice(self, cr, uid, ids, context=None): + # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise) + cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN + sale_order_line sol ON (sol.order_id = rel.order_id) + WHERE rel.invoice_id = ANY(%s)""", (list(ids),)) + return [i[0] for i in cr.fetchall()] + _name = 'sale.order.line' _description = 'Sales Order Line' _columns = { 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}), - 'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}), + 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."), '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', readonly=True), - 'invoiced': fields.boolean('Invoiced', readonly=True), + 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean', + store={ + 'account.invoice': (_order_lines_from_invoice, ['state'], 10), + 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}), 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}), 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, - help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."), + help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."), 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')), 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}), - 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'), + 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."), 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}), @@ -737,14 +719,13 @@ class sale_order_line(osv.osv): 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'), 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), } - _order = 'sequence, id' + _order = 'order_id desc, sequence, id' _defaults = { 'product_uom' : _get_uom_id, 'discount': 0.0, 'product_uom_qty': 1, 'product_uos_qty': 1, 'sequence': 10, - 'invoiced': 0, 'state': 'draft', 'type': 'make_to_stock', 'price_unit': 0.0, @@ -764,7 +745,7 @@ class sale_order_line(osv.osv): def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None): """Prepare the dict of values to create the new invoice line for a - sale order line. This method may be overridden to implement custom + sales order line. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). @@ -777,7 +758,7 @@ class sale_order_line(osv.osv): if not line.invoiced: if not account_id: if line.product_id: - account_id = line.product_id.product_tmpl_id.property_account_income.id + account_id = line.product_id.property_account_income.id if not account_id: account_id = line.product_id.categ_id.property_account_income_categ.id if not account_id: @@ -826,8 +807,7 @@ class sale_order_line(osv.osv): vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context) if vals: inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context) - cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id)) - self.write(cr, uid, [line.id], {'invoiced': True}) + self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context) sales.add(line.order_id.id) create_ids.append(inv_id) # Trigger workflow events @@ -839,7 +819,7 @@ class sale_order_line(osv.osv): def button_cancel(self, cr, uid, ids, context=None): for line in self.browse(cr, uid, ids, context=context): if line.invoiced: - raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.')) + raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.')) return self.write(cr, uid, ids, {'state': 'cancel'}) def button_confirm(self, cr, uid, ids, context=None): @@ -875,7 +855,7 @@ class sale_order_line(osv.osv): def copy_data(self, cr, uid, id, default=None, context=None): if not default: default = {} - default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []}) + default.update({'state': 'draft', 'invoice_lines': []}) return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context) def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, @@ -884,7 +864,7 @@ class sale_order_line(osv.osv): context = context or {} lang = lang or context.get('lang',False) if not partner_id: - raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.')) + raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.')) warning = {} product_uom_obj = self.pool.get('product.uom') partner_obj = self.pool.get('res.partner') @@ -902,8 +882,8 @@ class sale_order_line(osv.osv): date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT) result = {} - warning_msgs = {} - product_obj = product_obj.browse(cr, uid, product, context=context) + warning_msgs = '' + product_obj = product_obj.browse(cr, uid, product, context=context_partner) uom2 = False if uom: @@ -952,7 +932,6 @@ class sale_order_line(osv.osv): result['product_uos'] = product_obj.uos_id.id result['product_uos_qty'] = qty * product_obj.uos_coeff else: - result['product_uom'] = default_uom result['product_uos'] = False result['product_uos_qty'] = qty result['th_weight'] = q * product_obj.weight # Round the quantity up @@ -1006,17 +985,32 @@ class sale_order_line(osv.osv): raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,)) return super(sale_order_line, self).unlink(cr, uid, ids, context=context) -sale_order_line() -class mail_compose_message(osv.osv): +class mail_compose_message(osv.Model): _inherit = 'mail.compose.message' + def send_mail(self, cr, uid, ids, context=None): context = context or {} - if context.get('mark_so_as_sent', False) and context.get('default_res_id', False): + if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'): + context = dict(context, mail_post_autofollow=True) wf_service = netsvc.LocalService("workflow") - wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr) + wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr) return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) -mail_compose_message() + +class account_invoice(osv.Model): + _inherit = 'account.invoice' + + def unlink(self, cr, uid, ids, context=None): + """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """ + invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context) + #if we can't cancel all invoices, do nothing + if len(invoice_ids) == len(ids): + #Cancel invoice(s) first before deleting them so that if any sale order is associated with them + #it will trigger the workflow to put the sale order in an 'invoice exception' state + wf_service = netsvc.LocalService("workflow") + for id in ids: + wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr) + return super(account_invoice, self).unlink(cr, uid, ids, context=context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: