#This file is part of Tryton.  The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import copy
from decimal import Decimal
import datetime
from itertools import groupby
from functools import partial
from trytond.model import ModelWorkflow, ModelView, ModelSQL, fields
from trytond.modules.company import CompanyReport
from trytond.wizard import Wizard
from trytond.backend import TableHandler
from trytond.pyson import If, Eval, Bool, PYSONEncoder
from trytond.transaction import Transaction
from trytond.pool import Pool


class Sale(ModelWorkflow, ModelSQL, ModelView):
    'Sale'
    _name = 'sale.sale'
    _rec_name = 'reference'
    _description = __doc__

    company = fields.Many2One('company.company', 'Company', required=True,
        states={
            'readonly': (Eval('state') != 'draft') | Eval('lines', []),
            },
        domain=[
            ('id', If(Eval('context', {}).contains('company'), '=', '!='),
                Eval('context', {}).get('company', 0)),
            ],
        depends=['state', 'lines'])
    reference = fields.Char('Reference', readonly=True, select=1)
    description = fields.Char('Description',
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    state = fields.Selection([
        ('draft', 'Draft'),
        ('quotation', 'Quotation'),
        ('confirmed', 'Confirmed'),
        ('done', 'Done'),
        ('cancel', 'Canceled'),
    ], 'State', readonly=True, required=True)
    sale_date = fields.Date('Sale Date', required=True,
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    payment_term = fields.Many2One('account.invoice.payment_term',
        'Payment Term', required=True, states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    party = fields.Many2One('party.party', 'Party', change_default=True,
        required=True, select=1, states={
            'readonly': Eval('state') != 'draft',
            }, on_change=['party', 'payment_term'],
        depends=['state'])
    party_lang = fields.Function(fields.Char('Party Language',
        on_change_with=['party']), 'get_function_fields')
    invoice_address = fields.Many2One('party.address', 'Invoice Address',
        domain=[('party', '=', Eval('party'))], states={
            'readonly': Eval('state') != 'draft',
            }, depends=['state', 'party'])
    shipment_address = fields.Many2One('party.address', 'Shipment Address',
        domain=[('party', '=', Eval('party'))], states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['party', 'state'])
    warehouse = fields.Many2One('stock.location', 'Warehouse',
        domain=[('type', '=', 'warehouse')], required=True, states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    currency = fields.Many2One('currency.currency', 'Currency', required=True,
        states={
            'readonly': (Eval('state') != 'draft') |
                (Eval('lines', []) & Eval('currency', 0)),
            },
        depends=['state', 'lines'])
    currency_digits = fields.Function(fields.Integer('Currency Digits',
        on_change_with=['currency']), 'get_function_fields')
    lines = fields.One2Many('sale.line', 'sale', 'Lines', states={
            'readonly': Eval('state') != 'draft',
            }, on_change=['lines', 'currency', 'party'],
        depends=['party', 'state'])
    comment = fields.Text('Comment')
    untaxed_amount = fields.Function(fields.Numeric('Untaxed',
            digits=(16, Eval('currency_digits', 2)),
            depends=['currency_digits']), 'get_function_fields')
    tax_amount = fields.Function(fields.Numeric('Tax',
            digits=(16, Eval('currency_digits', 2)),
            depends=['currency_digits']), 'get_function_fields')
    total_amount = fields.Function(fields.Numeric('Total',
            digits=(16, Eval('currency_digits', 2)),
            depends=['currency_digits']), 'get_function_fields')
    invoice_method = fields.Selection([
            ('manual', 'Manual'),
            ('order', 'On Order Confirmed'),
            ('shipment', 'On Shipment Sent'),
            ],
        'Invoice Method', required=True, states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    invoice_state = fields.Selection([
        ('none', 'None'),
        ('waiting', 'Waiting'),
        ('paid', 'Paid'),
        ('exception', 'Exception'),
    ], 'Invoice State', readonly=True, required=True)
    invoices = fields.Many2Many('sale.sale-account.invoice',
            'sale', 'invoice', 'Invoices', readonly=True)
    invoices_ignored = fields.Many2Many('sale.sale-ignored-account.invoice',
            'sale', 'invoice', 'Ignored Invoices', readonly=True)
    invoices_recreated = fields.Many2Many('sale.sale-recreated-account.invoice',
            'sale', 'invoice', 'Recreated Invoices', readonly=True)
    invoice_paid = fields.Function(fields.Boolean('Invoices Paid'),
            'get_function_fields')
    invoice_exception = fields.Function(fields.Boolean('Invoices Exception'),
            'get_function_fields')
    shipment_method = fields.Selection([
            ('manual', 'Manual'),
            ('order', 'On Order Confirmed'),
            ('invoice', 'On Invoice Paid'),
            ], 'Shipment Method', required=True,
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    shipment_state = fields.Selection([
        ('none', 'None'),
        ('waiting', 'Waiting'),
        ('sent', 'Sent'),
        ('exception', 'Exception'),
    ], 'Shipment State', readonly=True, required=True)
    shipments = fields.Function(fields.One2Many('stock.shipment.out', None,
        'Shipments'), 'get_function_fields')
    moves = fields.Function(fields.One2Many('stock.move', None, 'Moves'),
            'get_function_fields')
    shipment_done = fields.Function(fields.Boolean('Shipment Done'),
            'get_function_fields')
    shipment_exception = fields.Function(fields.Boolean('Shipments Exception'),
            'get_function_fields')

    def __init__(self):
        super(Sale, self).__init__()
        self._order[0] = ('sale_date', 'DESC')
        self._order[0] = ('id', 'DESC')
        self._constraints += [
            ('check_method', 'wrong_method')
        ]
        self._error_messages.update({
            'wrong_method': 'Wrong combination of method!',
            'addresses_required': 'Invoice and Shipment addresses must be '
            'defined for the quotation.',
            'missing_account_receivable': 'It misses '
                    'an "Account Receivable" on the party "%s"!',
        })

    def init(self, module_name):
        cursor = Transaction().cursor
        # Migration from 1.2: packing renamed into shipment
        cursor.execute("UPDATE ir_model_data "
                "SET fs_id = REPLACE(fs_id, 'packing', 'shipment') "
                "WHERE fs_id like '%%packing%%' AND module = %s",
                (module_name,))
        cursor.execute("UPDATE ir_model_field "
                "SET relation = REPLACE(relation, 'packing', 'shipment'), "
                    "name = REPLACE(name, 'packing', 'shipment') "
                "WHERE (relation like '%%packing%%' "
                    "OR name like '%%packing%%') AND module = %s",
                (module_name,))
        table = TableHandler(cursor, self, module_name)
        table.column_rename('packing_state', 'shipment_state')
        table.column_rename('packing_method', 'shipment_method')
        table.column_rename('packing_address', 'shipment_address')

        super(Sale, self).init(module_name)

        # Migration from 1.2
        cursor.execute("UPDATE " + self._table + " "
                "SET invoice_method = 'shipment' "
                "WHERE invoice_method = 'packing'")

        # Add index on create_date
        table = TableHandler(cursor, self, module_name)
        table.index_action('create_date', action='add')

    def default_payment_term(self):
        payment_term_obj = Pool().get('account.invoice.payment_term')
        payment_term_ids = payment_term_obj.search(self.payment_term.domain)
        if len(payment_term_ids) == 1:
            return payment_term_ids[0]
        return False

    def default_warehouse(self):
        location_obj = Pool().get('stock.location')
        location_ids = location_obj.search(self.warehouse.domain)
        if len(location_ids) == 1:
            return location_ids[0]
        return False

    def default_company(self):
        return Transaction().context.get('company') or False

    def default_state(self):
        return 'draft'

    def default_sale_date(self):
        date_obj = Pool().get('ir.date')
        return date_obj.today()

    def default_currency(self):
        company_obj = Pool().get('company.company')
        currency_obj = Pool().get('currency.currency')
        company = Transaction().context.get('company')
        if company:
            return company_obj.browse(company).currency.id
        return False

    def default_currency_digits(self):
        company_obj = Pool().get('company.company')
        company = Transaction().context.get('company')
        if company:
            return company_obj.browse(company).currency.digits
        return 2

    def default_invoice_method(self):
        config_obj = Pool().get('sale.configuration')
        config = config_obj.browse(1)
        return config.sale_invoice_method

    def default_invoice_state(self):
        return 'none'

    def default_shipment_method(self):
        config_obj = Pool().get('sale.configuration')
        config = config_obj.browse(1)
        return config.sale_shipment_method

    def default_shipment_state(self):
        return 'none'

    def on_change_party(self, vals):
        pool = Pool()
        party_obj = pool.get('party.party')
        address_obj = pool.get('party.address')
        payment_term_obj = pool.get('account.invoice.payment_term')
        res = {
            'invoice_address': False,
            'shipment_address': False,
            'payment_term': False,
        }
        if vals.get('party'):
            party = party_obj.browse(vals['party'])
            res['invoice_address'] = party_obj.address_get(party.id,
                    type='invoice')
            res['shipment_address'] = party_obj.address_get(party.id,
                    type='delivery')
            if party.payment_term:
                res['payment_term'] = party.payment_term.id

        if res['invoice_address']:
            res['invoice_address.rec_name'] = address_obj.browse(
                    res['invoice_address']).rec_name
        if res['shipment_address']:
            res['shipment_address.rec_name'] = address_obj.browse(
                    res['shipment_address']).rec_name
        if not res['payment_term']:
            res['payment_term'] = self.default_payment_term()
        if res['payment_term']:
            res['payment_term.rec_name'] = payment_term_obj.browse(
                    res['payment_term']).rec_name
        return res

    def on_change_with_currency_digits(self, vals):
        currency_obj = Pool().get('currency.currency')
        if vals.get('currency'):
            currency = currency_obj.browse(vals['currency'])
            return currency.digits
        return 2

    def get_currency_digits(self, sales):
        '''
        Return the number of digits of the currency of each sales

        :param sales: a BrowseRecordList of puchases
        :return: a dictionary with sale id as key and
            number of digits as value
        '''
        res = {}
        for sale in sales:
            res[sale.id] = sale.currency.digits
        return res

    def get_tax_context(self, sale):
        party_obj = Pool().get('party.party')
        res = {}
        if isinstance(sale, dict):
            if sale.get('party'):
                party = party_obj.browse(sale['party'])
                if party.lang:
                    res['language'] = party.lang.code
        else:
            if sale.party.lang:
                res['language'] = sale.party.lang.code
        return res

    def on_change_with_party_lang(self, vals):
        party_obj = Pool().get('party.party')
        if vals.get('party'):
            party = party_obj.browse(vals['party'])
            if party.lang:
                return party.lang.code
        return 'en_US'

    def get_party_lang(self, sales):
        '''
        Return the code lang of the party for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            code lang as value
        '''
        res = {}
        for sale in sales:
            if sale.party.lang:
                res[sale.id] = sale.party.lang.code
            else:
                res[sale.id] = 'en_US'
        return res


    def on_change_lines(self, vals):
        pool = Pool()
        currency_obj = pool.get('currency.currency')
        tax_obj = pool.get('account.tax')
        invoice_obj = pool.get('account.invoice')

        res = {
            'untaxed_amount': Decimal('0.0'),
            'tax_amount': Decimal('0.0'),
            'total_amount': Decimal('0.0'),
        }

        currency = None
        if vals.get('currency'):
            currency = currency_obj.browse(vals['currency'])

        if vals.get('lines'):
            taxes = {}
            for line in vals['lines']:
                if line.get('type', 'line') != 'line':
                    continue
                res['untaxed_amount'] += line.get('amount', Decimal('0.0'))
                tax_list = ()
                with Transaction().set_context(self.get_tax_context(vals)):
                    tax_list = tax_obj.compute(line.get('taxes', []),
                            line.get('unit_price', Decimal('0.0')),
                            line.get('quantity', 0.0))
                for tax in tax_list:
                    key, val = invoice_obj._compute_tax(tax,'out_invoice')
                    if not key in taxes:
                        taxes[key] = val['amount']
                    else:
                        taxes[key] += val['amount']
            if currency:
                for key in taxes:
                    res['tax_amount'] += currency_obj.round(currency,
                            taxes[key])
        if currency:
            res['untaxed_amount'] = currency_obj.round(currency,
                    res['untaxed_amount'])
            res['tax_amount'] = currency_obj.round(currency,
                    res['tax_amount'])
        res['total_amount'] = res['untaxed_amount'] + res['tax_amount']
        if currency:
            res['total_amount'] = currency_obj.round(currency,
                    res['total_amount'])
        return res

    def get_function_fields(self, ids, names):
        '''
        Function to compute function fields for sale ids

        :param ids: the ids of the sales
        :param names: the list of field name to compute
        :return: a dictionary with all field names as key and
            a dictionary as value with id as key
        '''
        res = {}
        sales = self.browse(ids)
        if 'currency_digits' in names:
            res['currency_digits'] = self.get_currency_digits(sales)
        if 'party_lang' in names:
            res['party_lang'] = self.get_party_lang(sales)
        if 'untaxed_amount' in names:
            res['untaxed_amount'] = self.get_untaxed_amount(sales)
        if 'tax_amount' in names:
            res['tax_amount'] = self.get_tax_amount(sales)
        if 'total_amount' in names:
            res['total_amount'] = self.get_total_amount(sales)
        if 'invoice_paid' in names:
            res['invoice_paid'] = self.get_invoice_paid(sales)
        if 'invoice_exception' in names:
            res['invoice_exception'] = self.get_invoice_exception(sales)
        if 'shipments' in names:
            res['shipments'] = self.get_shipments(sales)
        if 'moves' in names:
            res['moves'] = self.get_moves(sales)
        if 'shipment_done' in names:
            res['shipment_done'] = self.get_shipment_done(sales)
        if 'shipment_exception' in names:
            res['shipment_exception'] = self.get_shipment_exception(sales)
        return res

    def get_untaxed_amount(self, sales):
        '''
        Compute the untaxed amount for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            untaxed amount as value
        '''
        currency_obj = Pool().get('currency.currency')
        res = {}
        for sale in sales:
            res.setdefault(sale.id, Decimal('0.0'))
            for line in sale.lines:
                if line.type != 'line':
                    continue
                res[sale.id] += line.amount
            res[sale.id] = currency_obj.round(sale.currency, res[sale.id])
        return res

    def get_tax_amount(self, sales):
        '''
        Compute tax amount for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            tax amount as value
        '''
        pool = Pool()
        currency_obj = pool.get('currency.currency')
        tax_obj = pool.get('account.tax')
        invoice_obj = pool.get('account.invoice')

        res = {}
        for sale in sales:
            res.setdefault(sale.id, Decimal('0.0'))
            taxes = {}
            for line in sale.lines:
                if line.type != 'line':
                    continue
                # Don't round on each line to handle rounding error
                tax_list = ()
                with Transaction().set_context(self.get_tax_context(sale)):
                    tax_list = tax_obj.compute(
                            [t.id for t in line.taxes], line.unit_price,
                            line.quantity)
                for tax in tax_list:
                    key, val = invoice_obj._compute_tax(tax, 'out_invoice')
                    if not key in taxes:
                        taxes[key] = val['amount']
                    else:
                        taxes[key] += val['amount']
            for key in taxes:
                res[sale.id] += currency_obj.round(sale.currency, taxes[key])
            res[sale.id] = currency_obj.round(sale.currency, res[sale.id])
        return res

    def get_total_amount(self, sales):
        '''
        Return the total amount of each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            total amount as value
        '''
        currency_obj = Pool().get('currency.currency')
        res = {}
        untaxed_amounts = self.get_untaxed_amount(sales)
        tax_amounts = self.get_tax_amount(sales)
        for sale in sales:
            res[sale.id] = currency_obj.round(sale.currency,
                    untaxed_amounts[sale.id] + tax_amounts[sale.id])
        return res

    def get_invoice_paid(self, sales):
        '''
        Return if all invoices have been paid for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a boolean as value
        '''
        res = {}
        for sale in sales:
            val = True
            skip_ids = set(x.id for x in sale.invoices_ignored)
            skip_ids.update(x.id for x in sale.invoices_recreated)
            for invoice in sale.invoices:
                if invoice.state != 'paid' \
                        and invoice.id not in skip_ids:
                    val = False
                    break
            res[sale.id] = val
        return res

    def get_invoice_exception(self, sales):
        '''
        Return if there is an invoice exception for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a boolean as value
        '''
        res = {}
        for sale in sales:
            val = False
            skip_ids = set(x.id for x in sale.invoices_ignored)
            skip_ids.update(x.id for x in sale.invoices_recreated)
            for invoice in sale.invoices:
                if invoice.state == 'cancel' \
                        and invoice.id not in skip_ids:
                    val = True
                    break
            res[sale.id] = val
        return res

    def get_shipments(self, sales):
        '''
        Return shipment_out ids for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a list of shipment_out id as value
        '''
        res = {}
        for sale in sales:
            res[sale.id] = []
            for line in sale.lines:
                for move in line.moves:
                    if move.shipment_out:
                        if move.shipment_out.id not in res[sale.id]:
                            res[sale.id].append(move.shipment_out.id)
        return res

    def get_moves(self, sales):
        '''
        Return move ids for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a list of move ids as value
        '''
        res = {}
        for sale in sales:
            res[sale.id] = []
            for line in sale.lines:
                res[sale.id].extend([x.id for x in line.moves])
        return res

    def get_shipment_done(self, sales):
        '''
        Return if all the shipments have been done for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a boolean as value
        '''
        res = {}
        for sale in sales:
            val = True
            for line in sale.lines:
                if not line.move_done:
                    val = False
                    break
            res[sale.id] = val
        return res

    def get_shipment_exception(self, sales):
        '''
        Return if there is a shipment exception for each sales

        :param sales: a BrowseRecordList of sales
        :return: a dictionary with sale id as key and
            a boolean as value
        '''
        res = {}
        for sale in sales:
            val = False
            for line in sale.lines:
                if line.move_exception:
                    val = True
                    break
            res[sale.id] = val
        return res

    def check_method(self, ids):
        '''
        Check the methods.
        '''
        for sale in self.browse(ids):
            if sale.invoice_method == 'shipment' \
                    and sale.shipment_method in ('invoice', 'manual'):
                return False
            if sale.shipment_method == 'invoice' \
                    and sale.invoice_method in ('shipment', 'manual'):
                return False
        return True

    def get_rec_name(self, ids, name):
        if not ids:
            return []
        res = {}
        for sale in self.browse(ids):
            res[sale.id] = sale.reference or str(sale.id) \
                    + ' - ' + sale.party.rec_name
        return res

    def search_rec_name(self, name, clause):
        names = clause[2].split(' - ', 1)
        res = [('reference', clause[1], names[0])]
        if len(names) != 1 and names[1]:
            res.append(('party', clause[1], names[1]))
        return res

    def copy(self, ids, default=None):
        if default is None:
            default = {}
        default = default.copy()
        default['state'] = 'draft'
        default['reference'] = False
        default['invoice_state'] = 'none'
        default['invoices'] = False
        default['invoices_ignored'] = False
        default['shipment_state'] = 'none'
        return super(Sale, self).copy(ids, default=default)

    def check_for_quotation(self, sale_id):
        sale = self.browse(sale_id)
        if not sale.invoice_address or not sale.shipment_address:
            self.raise_user_error('addresses_required')
        return True

    def set_reference(self, sale_id):
        '''
        Fill the reference field with the sale sequence

        :param sale_id: the id of the sale

        :return: True if succeed
        '''
        sequence_obj = Pool().get('ir.sequence')
        config_obj = Pool().get('sale.configuration')

        sale = self.browse(sale_id)

        if sale.reference:
            return True

        config = config_obj.browse(1)
        reference = sequence_obj.get_id(config.sale_sequence.id)
        self.write(sale_id, {
            'reference': reference,
            })
        return True

    def set_sale_date(self, sale_id):
        date_obj = Pool().get('ir.date')

        self.write(sale_id, {
                'sale_date': date_obj.today(),
                })
        return True

    def _get_invoice_line_sale_line(self, sale):
        '''
        Return invoice line values for each sale lines

        :param sale: the BrowseRecord of the sale

        :return: a dictionary with invoiced sale line id as key
            and a list of invoice lines values as value
        '''
        line_obj = Pool().get('sale.line')
        res = {}
        for line in sale.lines:
            val = line_obj.get_invoice_line(line)
            if val:
                res[line.id] = val
        return res

    def _get_invoice_sale(self, sale):
        '''
        Return invoice values for sale

        :param sale: the BrowseRecord of the sale

        :return: a dictionary with invoice fields as key and
            invoice values as value
        '''
        journal_obj = Pool().get('account.journal')

        journal_id = journal_obj.search([
            ('type', '=', 'revenue'),
            ], limit=1)
        if journal_id:
            journal_id = journal_id[0]

        res = {
            'company': sale.company.id,
            'type': 'out_invoice',
            'reference': sale.reference,
            'journal': journal_id,
            'party': sale.party.id,
            'invoice_address': sale.invoice_address.id,
            'currency': sale.currency.id,
            'account': sale.party.account_receivable.id,
            'payment_term': sale.payment_term.id,
            }
        return res

    def create_invoice(self, sale_id):
        '''
        Create an invoice for the sale

        :param sale_id: the sale id

        :return: the created invoice id or None
        '''
        pool = Pool()
        invoice_obj = pool.get('account.invoice')
        invoice_line_obj = pool.get('account.invoice.line')
        sale_line_obj = pool.get('sale.line')

        sale = self.browse(sale_id)

        if not sale.party.account_receivable:
            self.raise_user_error('missing_account_receivable',
                    error_args=(sale.party.rec_name,))

        invoice_lines = self._get_invoice_line_sale_line(sale)
        if not invoice_lines:
            return


        vals = self._get_invoice_sale(sale)
        with Transaction().set_user(0, set_context=True):
            invoice_id = invoice_obj.create(vals)

        for line in sale.lines:
            if line.id not in invoice_lines:
                continue
            for vals in invoice_lines[line.id]:
                vals['invoice'] = invoice_id
                with Transaction().set_user(0, set_context=True):
                    invoice_line_id = invoice_line_obj.create(vals)
                sale_line_obj.write(line.id, {
                    'invoice_lines': [('add', invoice_line_id)],
                    })

        with Transaction().set_user(0, set_context=True):
            invoice_obj.update_taxes([invoice_id])

        self.write(sale_id, {
            'invoices': [('add', invoice_id)],
        })
        return invoice_id

    def _get_move_sale_line(self, sale):
        '''
        Return a dictionary of move values for each sale lines

        :param sale: the BrowseRecord of the sale

        :return: a dictionary with move as key and move values as value
        '''
        line_obj = Pool().get('sale.line')
        res = {}
        for line in sale.lines:
            val = line_obj.get_move(line)
            if val:
                res[line.id] = val
        return res

    def _group_shipment_key(self, moves, move):
        '''
        The key to group moves by shipments

        :param moves: a list of moves values
        :param move: a tuple of line id and a dictionary of the move values

        :return: a list of key-value as tuples of the shipment
        '''
        planned_date = max(m['planned_date'] for m in moves)
        return (('planned_date', planned_date),)

    def create_shipment(self, sale_id):
        '''
        Create a shipment for the sale

        :param sale_id: the sale id

        :return: the list of created shipment id or None
        '''
        pool = Pool()
        shipment_obj = pool.get('stock.shipment.out')
        move_obj = pool.get('stock.move')
        sale_line_obj = pool.get('sale.line')

        sale = self.browse(sale_id)

        moves = self._get_move_sale_line(sale)
        if not moves:
            return
        keyfunc = partial(self._group_shipment_key, moves.values())
        moves = moves.items()
        moves = sorted(moves, key=keyfunc)

        shipments = []
        with Transaction().set_user(0, set_context=True):
            for key, grouped_moves in groupby(moves, key=keyfunc):
                values = {
                    'customer': sale.party.id,
                    'delivery_address': sale.shipment_address.id,
                    'reference': sale.reference,
                    'warehouse': sale.warehouse.id,
                    'company': sale.company.id,
                    }
                values.update(dict(key))
                shipment_id = shipment_obj.create(values)
                shipments.append(shipment_id)
                for line_id, values in grouped_moves:
                    values['shipment_out'] = shipment_id
                    move_id = move_obj.create(values)
                    sale_line_obj.write(line_id, {
                        'moves': [('add', move_id)],
                    })
                shipment_obj.workflow_trigger_validate(shipment_id, 'waiting')
        return shipments

    def wkf_draft(self, sale):
        self.write(sale.id, {'state': 'draft'})

    def wkf_quotation(self, sale):
        self.check_for_quotation(sale.id)
        self.set_reference(sale.id)
        self.write(sale.id, {'state': 'quotation'})

    def wkf_confirmed(self, sale):
        self.set_sale_date(sale.id)
        self.write(sale.id, {'state': 'confirmed'})

    def wkf_waiting_invoice_sale(self, sale):
        self.create_invoice(sale.id)
        self.write(sale.id, {'invoice_state': 'waiting'})

    def wkf_invoice_sale_exception(self, sale):
        self.write(sale.id, {'invoice_state': 'exception'})

    def wkf_invoice_sale_done(self, sale):
        self.write(sale.id, {'invoice_state': 'paid'})

    def wkf_shipment_invoice(self, sale):
        self.create_shipment(sale.id)

    def wkf_waiting_shipment_invoice(self, sale):
        self.write(sale.id, {'shipment_state': 'waiting'})
        self.create_shipment(sale.id)

    def wkf_shipment_invoice_exception(self, sale):
        self.write(sale.id, {'shipment_state': 'exception'})

    def wkf_shipment_invoice_done(self, sale):
        self.write(sale.id, {'shipment_state': 'sent'})

    def wkf_shipment(self, sale):
        self.create_shipment(sale.id)

    def wkf_waiting_shipment(self, sale):
        self.write(sale.id, {'shipment_state': 'waiting'})
        self.create_shipment(sale.id)

    def wkf_shipment_exception(self, sale):
        self.write(sale.id, {'shipment_state': 'exception'})

    def wkf_invoice_shipment(self, sale):
        self.create_invoice(sale.id)
        self.write(sale.id, {'invoice_state': 'waiting'})

    def wkf_waiting_invoice_shipment(self, sale):
        self.write(sale.id,
            {'invoice_state': 'waiting', 'shipment_state': 'sent'})

    def wkf_invoice_shipment_exception(self, sale):
        self.write(sale.id, {'invoice_state': 'exception'})

    def wkf_invoice_shipment_done(self, sale):
        self.write(sale.id, {'invoice_state': 'paid'})

    def wkf_invoice_shipment_method_done(self, sale):
        self.write(sale.id, {'shipment_state': 'sent'})

    def wkf_done(self, sale):
        self.write(sale.id, {'state': 'done'})

    def wkf_cancel(self, sale):
        self.write(sale.id, {'state': 'cancel'})

    def wkf_draft2quotation(self, sale):
        return bool(sale.lines)

    def wkf_invoice_method2waiting_invoice_sale(self, sale):
        return sale.invoice_method == 'order'

    def wkf_invoice_method2invoice_method_done(self, sale):
        return sale.invoice_method != 'order'

    def wkf_triggered_invoices(self, sale):
        return [x.id for x in sale.invoices]

    def wkf_waiting_invoice_sale2invoice_sale_exception(self, sale):
        return sale.invoice_exception

    def wkf_waiting_invoice_sale2invoice_sale_done(self, sale):
        return sale.invoice_paid

    def wkf_shipment_invoice_method2shipment_invoice_method_done(self, sale):
        return sale.shipment_method != 'invoice'

    def wkf_shipment_invoice_method2shipment_invoice(self, sale):
        return self.shipment_method == 'invoice'

    def wkf_triggered_shipments(self, sale):
        return [x.id for x in sale.shipments]

    def wkf_waiting_shipment_invoice2shipment_invoice_exception(self, sale):
        return sale.shipment_exception

    def wkf_waiting_shipment_invoice2shipment_invoice_done(self, sale):
        return sale.shipment_done

    def wkf_shipment_method2shipment_method_done(self, sale):
        return sale.shipment_method != 'order'

    def wkf_shipment_method2shipment(self, sale):
        return sale.shipment_method == 'order'

    def wkf_waiting_shipment2shipment_exception(self, sale):
        return sale.shipment_exception

    def wkf_waiting_shipment2invoice_shipment_method(self, sale):
        return not sale.shipment_exception

    def wkf_waiting_shipment2invoice_shipment_method_nosignal(self, sale):
        return sale.shipment_done

    def wkf_invoice_shipment_method2invoice_shipment(self, sale):
        return sale.invoice_method == 'shipment'

    def wkf_invoice_shipment_method2invoice_shipment_method_done(self, sale):
        return sale.invoice_method != 'shipment' and sale.shipment_done

    def wkf_invoice_shipment_method2waiting_shipment(self, sale):
        return sale.invoice_method != 'shipment' and not sale.shipment_done

    def wkf_invoice_shipment2waiting_shipment(self, sale):
        return not sale.shipment_done

    def wkf_invoice_shipment2waiting_invoice_shipment(self, sale):
        return sale.shipment_done

    def wkf_waiting_invoice_shipment2invoice_shipment_exception(self, sale):
        return sale.invoice_exception

    def wkf_waiting_invoice_shipment2invoice_shipment_done(self, sale):
        return sale.invoice_paid

Sale()


class SaleInvoice(ModelSQL):
    'Sale - Invoice'
    _name = 'sale.sale-account.invoice'
    _table = 'sale_invoices_rel'
    _description = __doc__
    sale = fields.Many2One('sale.sale', 'Sale', ondelete='CASCADE', select=1,
            required=True)
    invoice = fields.Many2One('account.invoice', 'Invoice',
            ondelete='RESTRICT', select=1, required=True)

SaleInvoice()


class SaleIgnoredInvoice(ModelSQL):
    'Sale - Ignored Invoice'
    _name = 'sale.sale-ignored-account.invoice'
    _table = 'sale_invoice_ignored_rel'
    _description = __doc__
    sale = fields.Many2One('sale.sale', 'Sale', ondelete='CASCADE', select=1,
            required=True)
    invoice = fields.Many2One('account.invoice', 'Invoice',
            ondelete='RESTRICT', select=1, required=True)

SaleIgnoredInvoice()


class SaleRecreatedInvoice(ModelSQL):
    'Sale - Recreated Invoice'
    _name = 'sale.sale-recreated-account.invoice'
    _table = 'sale_invoice_recreated_rel'
    _description = __doc__
    sale = fields.Many2One('sale.sale', 'Sale', ondelete='CASCADE', select=1,
            required=True)
    invoice = fields.Many2One('account.invoice', 'Invoice',
            ondelete='RESTRICT', select=1, required=True)

SaleRecreatedInvoice()


class SaleLine(ModelSQL, ModelView):
    'Sale Line'
    _name = 'sale.line'
    _rec_name = 'description'
    _description = __doc__

    sale = fields.Many2One('sale.sale', 'Sale', ondelete='CASCADE', select=1)
    sequence = fields.Integer('Sequence')
    type = fields.Selection([
        ('line', 'Line'),
        ('subtotal', 'Subtotal'),
        ('title', 'Title'),
        ('comment', 'Comment'),
        ], 'Type', select=1, required=True)
    quantity = fields.Float('Quantity',
        digits=(16, Eval('unit_digits', 2)),
        states={
            'invisible': Eval('type') != 'line',
            'required': Eval('type') == 'line',
            'readonly': ~Eval('_parent_sale', {}),
            }, on_change=['product', 'quantity', 'unit',
            '_parent_sale.currency', '_parent_sale.party'],
        depends=['type', 'unit_digits'])
    unit = fields.Many2One('product.uom', 'Unit',
            states={
                'required': Bool(Eval('product')),
                'invisible': Eval('type') != 'line',
                'readonly': ~Eval('_parent_sale', {}),
            }, domain=[
                ('category', '=',
                    (Eval('product'), 'product.default_uom.category')),
            ],
            context={
                'category': (Eval('product'), 'product.default_uom.category'),
            },
            on_change=['product', 'quantity', 'unit', '_parent_sale.currency',
                '_parent_sale.party'],
            depends=['product', 'type'])
    unit_digits = fields.Function(fields.Integer('Unit Digits',
        on_change_with=['unit']), 'get_unit_digits')
    product = fields.Many2One('product.product', 'Product',
        domain=[('salable', '=', True)],
        states={
            'invisible': Eval('type') != 'line',
            'readonly': ~Eval('_parent_sale', {}),
            },
        on_change=['product', 'unit', 'quantity', 'description',
            '_parent_sale.party', '_parent_sale.currency'],
        context={
            'locations': If(Bool(Eval('_parent_sale', {}).get( 'warehouse')),
                [Eval('_parent_sale', {}).get('warehouse', 0)], []),
            'stock_date_end': Eval('_parent_sale', {}).get('sale_date'),
            'salable': True,
            'stock_skip_warehouse': True,
            }, depends=['type'])
    unit_price = fields.Numeric('Unit Price', digits=(16, 4),
        states={
            'invisible': Eval('type') != 'line',
            'required': Eval('type') == 'line',
            }, depends=['type'])
    amount = fields.Function(fields.Numeric('Amount',
            digits=(16, Eval('_parent_sale', {}).get('currency_digits', 2)),
            states={
                'invisible': ~Eval('type').in_(['line', 'subtotal']),
                'readonly': ~Eval('_parent_sale'),
                }, on_change_with=['type', 'quantity', 'unit_price',
                '_parent_sale.currency'],
            depends=['type']), 'get_amount')
    description = fields.Text('Description', size=None, required=True)
    note = fields.Text('Note')
    taxes = fields.Many2Many('sale.line-account.tax', 'line', 'tax', 'Taxes',
        domain=[('parent', '=', False)], states={
            'invisible': Eval('type') != 'line',
            }, depends=['type'])
    invoice_lines = fields.Many2Many('sale.line-account.invoice.line',
            'sale_line', 'invoice_line', 'Invoice Lines', readonly=True)
    moves = fields.One2Many('stock.move', 'sale_line', 'Moves',
            readonly=True)
    moves_ignored = fields.Many2Many('sale.line-ignored-stock.move',
            'sale_line', 'move', 'Ignored Moves', readonly=True)
    moves_recreated = fields.Many2Many('sale.line-recreated-stock.move',
            'sale_line', 'move', 'Recreated Moves', readonly=True)
    move_done = fields.Function(fields.Boolean('Moves Done'), 'get_move_done')
    move_exception = fields.Function(fields.Boolean('Moves Exception'),
            'get_move_exception')

    def __init__(self):
        super(SaleLine, self).__init__()
        self._order.insert(0, ('sequence', 'ASC'))
        self._error_messages.update({
            'customer_location_required': 'The customer location is required!',
            'missing_account_revenue': 'It misses '
                    'an "Account Revenue" on product "%s"!',
            'missing_account_revenue_property': 'It misses '
                    'an "account Revenue" default property!',
            })

    def init(self, module_name):
        super(SaleLine, self).init(module_name)
        cursor = Transaction().cursor
        table = TableHandler(cursor, self, module_name)

        # Migration from 1.0 comment change into note
        if table.column_exist('comment'):
            cursor.execute('UPDATE "' + self._table + '" SET note = comment')
            table.drop_column('comment', exception=True)

    def default_type(self):
        return 'line'

    def default_quantity(self):
        return 0.0

    def default_unit_price(self):
        return Decimal('0.0')

    def on_change_with_unit_digits(self, vals):
        uom_obj = Pool().get('product.uom')
        if vals.get('unit'):
            uom = uom_obj.browse(vals['unit'])
            return uom.digits
        return 2

    def get_unit_digits(self, ids, name):
        res = {}
        for line in self.browse(ids):
            if line.unit:
                res[line.id] = line.unit.digits
            else:
                res[line.id] = 2
        return res

    def get_move_done(self, ids, name):
        uom_obj = Pool().get('product.uom')
        res = {}
        for line in self.browse(ids):
            val = True
            if not line.product:
                res[line.id] = True
                continue
            if line.product.type == 'service':
                res[line.id] = True
                continue
            skip_ids = set(x.id for x in line.moves_ignored)
            skip_ids.update(x.id for x in line.moves_recreated)
            quantity = line.quantity
            for move in line.moves:
                if move.state != 'done' \
                        and move.id not in skip_ids:
                    val = False
                    break
                quantity -= uom_obj.compute_qty(move.uom, move.quantity,
                        line.unit)
            if val:
                if quantity > 0.0:
                    val = False
            res[line.id] = val
        return res

    def get_move_exception(self, ids, name):
        res = {}
        for line in self.browse(ids):
            val = False
            skip_ids = set(x.id for x in line.moves_ignored)
            skip_ids.update(x.id for x in line.moves_recreated)
            for move in line.moves:
                if move.state == 'cancel' \
                        and move.id not in skip_ids:
                    val = True
                    break
            res[line.id] = val
        return res

    def _get_tax_rule_pattern(self, party, vals):
        '''
        Get tax rule pattern

        :param party: the BrowseRecord of the party
        :param vals: a dictionary with value from on_change
        :return: a dictionary to use as pattern for tax rule
        '''
        res = {}
        return res

    def _get_context_sale_price(self, product, vals):
        context = {}
        if vals.get('_parent_sale.currency'):
            context['currency'] = vals['_parent_sale.currency']
        if vals.get('_parent_sale.party'):
            context['customer'] = vals['_parent_sale.party']
        if vals.get('_parent_sale.sale_date'):
            context['sale_date'] = vals['_parent_sale.sale_date']
        if vals.get('unit'):
            context['uom'] = vals['unit']
        else:
            context['uom'] = product.sale_uom.id
        return context

    def on_change_product(self, vals):
        pool = Pool()
        party_obj = pool.get('party.party')
        product_obj = pool.get('product.product')
        uom_obj = pool.get('product.uom')
        tax_rule_obj = pool.get('account.tax.rule')

        if not vals.get('product'):
            return {}
        res = {}

        party = None
        party_context = {}
        if vals.get('_parent_sale.party'):
            party = party_obj.browse(vals['_parent_sale.party'])
            if party.lang:
                party_context['language'] = party.lang.code

        product = product_obj.browse(vals['product'])

        with Transaction().set_context(
                self._get_context_sale_price(product,vals)):
            res['unit_price'] = product_obj.get_sale_price([product.id],
                    vals.get('quantity', 0))[product.id]
            if res['unit_price']:
                res['unit_price'] = res['unit_price'].quantize(
                    Decimal(1) / 10 ** self.unit_price.digits[1])
        res['taxes'] = []
        pattern = self._get_tax_rule_pattern(party, vals)
        for tax in product.customer_taxes_used:
            if party and party.customer_tax_rule:
                tax_ids = tax_rule_obj.apply(party.customer_tax_rule, tax,
                        pattern)
                if tax_ids:
                    res['taxes'].extend(tax_ids)
                continue
            res['taxes'].append(tax.id)
        if party and party.customer_tax_rule:
            tax_ids = tax_rule_obj.apply(party.customer_tax_rule, False,
                    pattern)
            if tax_ids:
                res['taxes'].extend(tax_ids)

        if not vals.get('description'):
            with Transaction().set_context(party_context):
                res['description'] = product_obj.browse(product.id).rec_name

        category = product.sale_uom.category
        if not vals.get('unit') \
                or vals.get('unit') not in [x.id for x in category.uoms]:
            res['unit'] = product.sale_uom.id
            res['unit.rec_name'] = product.sale_uom.rec_name
            res['unit_digits'] = product.sale_uom.digits

        vals = vals.copy()
        vals['unit_price'] = res['unit_price']
        vals['type'] = 'line'
        res['amount'] = self.on_change_with_amount(vals)
        return res

    def on_change_quantity(self, vals):
        product_obj = Pool().get('product.product')

        if not vals.get('product'):
            return {}
        res = {}

        product = product_obj.browse(vals['product'])

        with Transaction().set_context(
                self._get_context_sale_price(product, vals)):
            res['unit_price'] = product_obj.get_sale_price([vals['product']],
                    vals.get('quantity', 0))[vals['product']]
            if res['unit_price']:
                res['unit_price'] = res['unit_price'].quantize(
                    Decimal(1) / 10 ** self.unit_price.digits[1])
        return res

    def on_change_unit(self, vals):
        return self.on_change_quantity(vals)

    def on_change_with_amount(self, vals):
        currency_obj = Pool().get('currency.currency')
        if vals.get('type') == 'line':
            currency = vals.get('_parent_sale.currency')
            if currency and isinstance(currency, (int, long)):
                currency = currency_obj.browse(vals['_parent_sale.currency'])
            amount = Decimal(str(vals.get('quantity') or '0.0')) * \
                    (vals.get('unit_price') or Decimal('0.0'))
            if currency:
                return currency_obj.round(currency, amount)
            return amount
        return Decimal('0.0')

    def get_amount(self, ids, name):
        currency_obj = Pool().get('currency.currency')
        res = {}
        for line in self.browse(ids):
            if line.type == 'line':
                res[line.id] = currency_obj.round(line.sale.currency,
                        Decimal(str(line.quantity)) * line.unit_price)
            elif line.type == 'subtotal':
                res[line.id] = Decimal('0.0')
                for line2 in line.sale.lines:
                    if line2.type == 'line':
                        res[line.id] += currency_obj.round(line2.sale.currency,
                                Decimal(str(line2.quantity)) *
                                        line2.unit_price)
                    elif line2.type == 'subtotal':
                        if line.id == line2.id:
                            break
                        res[line.id] = Decimal('0.0')
            else:
                res[line.id] = Decimal('0.0')
        return res

    def get_invoice_line(self, line):
        '''
        Return invoice line values for sale line

        :param line: the BrowseRecord of the line

        :return: a list of invoice line values
        '''
        uom_obj = Pool().get('product.uom')
        property_obj = Pool().get('ir.property')

        res = {}
        res['sequence'] = line.sequence
        res['type'] = line.type
        res['description'] = line.description
        res['note'] = line.note
        if line.type != 'line':
            return [res]

        if (line.sale.invoice_method == 'order'
                or not line.product
                or line.product.type == 'service'):
            quantity = line.quantity
        else:
            quantity = 0.0
            for move in line.moves:
                if move.state == 'done':
                    quantity += uom_obj.compute_qty(move.uom, move.quantity,
                            line.unit)

        ignored_ids = set(
            l.id for i in line.sale.invoices_ignored for l in i.lines)
        for invoice_line in line.invoice_lines:
            if ((invoice_line.invoice and
                    invoice_line.invoice.state != 'cancel') or
                invoice_line.id in ignored_ids):
                quantity -= uom_obj.compute_qty(invoice_line.unit,
                        invoice_line.quantity, line.unit)
        res['quantity'] = quantity

        if res['quantity'] <= 0.0:
            return []
        res['unit'] = line.unit.id
        res['product'] = line.product.id
        res['unit_price'] = line.unit_price
        res['taxes'] = [('set', [x.id for x in line.taxes])]
        if line.product:
            res['account'] = line.product.account_revenue_used.id
            if not res['account']:
                self.raise_user_error('missing_account_revenue',
                        error_args=(line.product.rec_name,))
        else:
            for model in ('product.template', 'product.category'):
                res['account'] = property_obj.get('account_revenue', model)
                if res['account']:
                    break
            if not res['account']:
                self.raise_user_error('missing_account_revenue_property')
        return [res]

    def copy(self, ids, default=None):
        if default is None:
            default = {}
        default = default.copy()
        default['moves'] = False
        default['moves_ignored'] = False
        default['moves_recreated'] = False
        default['invoice_lines'] = False
        return super(SaleLine, self).copy(ids, default=default)

    def get_move(self, line):
        '''
        Return move values for the sale line

        :param line: the BrowseRecord of the line

        :return: a dictionary of values of move
        '''
        uom_obj = Pool().get('product.uom')
        product_obj = Pool().get('product.product')

        res = {}
        if line.type != 'line':
            return
        if not line.product:
            return
        if line.product.type == 'service':
            return
        skip_ids = set(x.id for x in line.moves_recreated)
        quantity = line.quantity
        for move in line.moves:
            if move.id not in skip_ids:
                quantity -= uom_obj.compute_qty(move.uom, move.quantity,
                        line.unit)
        if quantity <= 0.0:
            return
        if not line.sale.party.customer_location:
            self.raise_user_error('customer_location_required')
        res['quantity'] = quantity
        res['uom'] = line.unit.id
        res['product'] = line.product.id
        res['from_location'] = line.sale.warehouse.output_location.id
        res['to_location'] = line.sale.party.customer_location.id
        res['state'] = 'draft'
        res['company'] = line.sale.company.id
        res['unit_price'] = line.unit_price
        res['currency'] = line.sale.currency.id
        res['planned_date'] = product_obj.compute_delivery_date(
            line.product)
        return res

SaleLine()


class SaleLineTax(ModelSQL):
    'Sale Line - Tax'
    _name = 'sale.line-account.tax'
    _table = 'sale_line_account_tax'
    _description = __doc__
    line = fields.Many2One('sale.line', 'Sale Line', ondelete='CASCADE',
            select=1, required=True)
    tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
            select=1, required=True)

SaleLineTax()


class SaleLineInvoiceLine(ModelSQL):
    'Sale Line - Invoice Line'
    _name = 'sale.line-account.invoice.line'
    _table = 'sale_line_invoice_lines_rel'
    _description = __doc__
    sale_line = fields.Many2One('sale.line', 'Sale Line', ondelete='CASCADE',
            select=1, required=True)
    invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
            ondelete='RESTRICT', select=1, required=True)

SaleLineInvoiceLine()


class SaleLineIgnoredMove(ModelSQL):
    'Sale Line - Ignored Move'
    _name = 'sale.line-ignored-stock.move'
    _table = 'sale_line_moves_ignored_rel'
    _description = __doc__
    sale_line = fields.Many2One('sale.line', 'Sale Line', ondelete='CASCADE',
            select=1, required=True)
    move = fields.Many2One('stock.move', 'Move', ondelete='RESTRICT',
            select=1, required=True)

SaleLineIgnoredMove()


class SaleLineRecreatedMove(ModelSQL):
    'Sale Line - Recreated Move'
    _name = 'sale.line-recreated-stock.move'
    _table = 'sale_line_moves_recreated_rel'
    _description = __doc__
    sale_line = fields.Many2One('sale.line', 'Sale Line', ondelete='CASCADE',
            select=1, required=True)
    move = fields.Many2One('stock.move', 'Move', ondelete='RESTRICT',
            select=1, required=True)

SaleLineRecreatedMove()


class SaleReport(CompanyReport):
    _name = 'sale.sale'

SaleReport()


class Template(ModelSQL, ModelView):
    _name = 'product.template'

    salable = fields.Boolean('Salable', states={
            'readonly': ~Eval('active', True),
            }, depends=['active'])
    sale_uom = fields.Many2One('product.uom', 'Sale UOM', states={
            'readonly': ~Eval('active', True),
            'invisible': ~Eval('salable', False),
            'required': Eval('salable', False),
            },
        domain=[
            ('category', '=', (Eval('default_uom'), 'uom.category')),
            ],
        context={'category': (Eval('default_uom'), 'uom.category')},
        on_change_with=['default_uom', 'sale_uom', 'salable'],
        depends=['active', 'salable', 'default_uom'])
    delivery_time = fields.Integer('Delivery Time', states={
            'readonly': ~Eval('active', True),
            'invisible': ~Eval('salable', False),
            },
        depends=['active', 'salable'],
        help='In number of days')

    def __init__(self):
        super(Template, self).__init__()
        self.account_revenue = copy.copy(self.account_revenue)
        self.account_revenue.states = copy.copy(self.account_revenue.states)
        required = ~Eval('account_category', False) & Eval('salable', False)
        if not self.account_revenue.states.get('required'):
            self.account_revenue.states['required'] = required
        else:
            self.account_revenue.states['required'] = (
                    self.account_revenue.states['required'] | required)
        if 'account_category' not in self.account_revenue.depends:
            self.account_revenue = copy.copy(self.account_revenue)
            self.account_revenue.depends = \
                    copy.copy(self.account_revenue.depends)
            self.account_revenue.depends.append('account_category')
        if 'salable' not in self.account_revenue.depends:
            self.account_revenue = copy.copy(self.account_revenue)
            self.account_revenue.depends = \
                    copy.copy(self.account_revenue.depends)
            self.account_revenue.depends.append('salable')
        self._reset_columns()

    def default_salable(self):
        return True if Transaction().context.get('salable') else False

    def on_change_with_sale_uom(self, vals):
        uom_obj = Pool().get('product.uom')
        res = False

        if vals.get('default_uom'):
            default_uom = uom_obj.browse(vals['default_uom'])
            if vals.get('sale_uom'):
                sale_uom = uom_obj.browse(vals['sale_uom'])
                if default_uom.category.id == sale_uom.category.id:
                    res = sale_uom.id
                else:
                    res = default_uom.id
            else:
                res = default_uom.id
        return res

Template()


class Product(ModelSQL, ModelView):
    _name = 'product.product'

    def get_sale_price(self, ids, quantity=0):
        '''
        Return the sale price for product ids.

        :param ids: the product ids
        :param quantity: the quantity of the products
            uom: the unit of measure
            currency: the currency id for the returned price
        :return: a dictionary with for each product ids keys the computed price
        '''
        pool = Pool()
        uom_obj = pool.get('product.uom')
        user_obj = pool.get('res.user')
        currency_obj = pool.get('currency.currency')
        date_obj = pool.get('ir.date')

        today = date_obj.today()
        res = {}

        uom = None
        if Transaction().context.get('uom'):
            uom = uom_obj.browse(Transaction().context.get('uom'))

        currency = None
        if Transaction().context.get('currency'):
            currency = currency_obj.browse(
                    Transaction().context.get('currency'))

        user2 = user_obj.browse(Transaction().user)

        for product in self.browse(ids):
            res[product.id] = product.list_price
            if uom:
                res[product.id] = uom_obj.compute_price(
                        product.default_uom, res[product.id], uom)
            if currency and user2.company:
                if user2.company.currency.id != currency.id:
                    date = Transaction().context.get('sale_date') or today
                    with Transaction().set_context(date=date):
                        res[product.id] = currency_obj.compute(
                                user2.company.currency.id, res[product.id],
                                currency.id, round=False)
        return res

    def compute_delivery_date(self, product, date=None):
        '''
        Compute the delivery date for the Product at a the given date
        '''
        date_obj = Pool().get('ir.date')

        if not date:
            date = date_obj.today()
        return date + datetime.timedelta(product.delivery_time)

Product()


class ShipmentOut(ModelSQL, ModelView):
    _name = 'stock.shipment.out'

    def __init__(self):
        super(ShipmentOut, self).__init__()
        self._error_messages.update({
                'reset_move': 'You cannot reset to draft a move generated '
                    'by a sale.',
            })

    def write(self, ids, vals):
        sale_obj = Pool().get('sale.sale')
        sale_line_obj = Pool().get('sale.line')

        res = super(ShipmentOut, self).write(ids, vals)

        if 'state' in vals and vals['state'] in ('done', 'cancel'):
            sale_ids = []
            move_ids = []
            if isinstance(ids, (int, long)):
                ids = [ids]
            for shipment in self.browse(ids):
                move_ids.extend([x.id for x in shipment.outgoing_moves])

            sale_line_ids = sale_line_obj.search([
                ('moves', 'in', move_ids),
                ])
            if sale_line_ids:
                for sale_line in sale_line_obj.browse(sale_line_ids):
                    if sale_line.sale.id not in sale_ids:
                        sale_ids.append(sale_line.sale.id)

            sale_obj.workflow_trigger_validate(sale_ids, 'shipment_update')
        return res

    def button_draft(self, ids):
        for shipment in self.browse(ids):
            for move in shipment.outgoing_moves:
                if move.state == 'cancel' and move.sale_line:
                    self.raise_user_error('reset_move')

        return super(ShipmentOut, self).button_draft(ids)

ShipmentOut()


class Move(ModelSQL, ModelView):
    _name = 'stock.move'

    sale_line = fields.Many2One('sale.line', 'Sale Line', select=1,
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    sale = fields.Function(fields.Many2One('sale.sale', 'Sale', select=1),
            'get_sale', searcher='search_sale')
    sale_exception_state = fields.Function(fields.Selection([
        ('', ''),
        ('ignored', 'Ignored'),
        ('recreated', 'Recreated'),
        ], 'Exception State'), 'get_sale_exception_state')

    def get_sale(self, ids, name):
        sale_obj = Pool().get('sale.sale')

        res = {}
        for move in self.browse(ids):
            res[move.id] = False
            if move.sale_line:
                res[move.id] = move.sale_line.sale.id
        return res

    def search_sale(self, name, clause):
        return [('sale_line.' + name,) + clause[1:]]

    def get_sale_exception_state(self, ids, name):
        res = {}.fromkeys(ids, '')
        for move in self.browse(ids):
            if not move.sale_line:
                continue
            if move.id in (x.id for x in move.sale_line.moves_recreated):
                res[move.id] = 'recreated'
            if move.id in (x.id for x in move.sale_line.moves_ignored):
                res[move.id] = 'ignored'
        return res

    def write(self, ids, vals):
        sale_obj = Pool().get('sale.sale')
        sale_line_obj = Pool().get('sale.line')

        res = super(Move, self).write(ids, vals)
        if 'state' in vals and vals['state'] in ('cancel',):
            if isinstance(ids, (int, long)):
                ids = [ids]
            sale_ids = set()
            sale_line_ids = sale_line_obj.search([
                ('moves', 'in', ids),
                ])
            if sale_line_ids:
                for sale_line in sale_line_obj.browse(sale_line_ids):
                    sale_ids.add(sale_line.sale.id)
            if sale_ids:
                sale_obj.workflow_trigger_validate(list(sale_ids),
                        'shipment_update')
        return res

    def delete(self, ids):
        sale_obj = Pool().get('sale.sale')
        sale_line_obj = Pool().get('sale.line')

        if isinstance(ids, (int, long)):
            ids = [ids]

        sale_ids = set()
        sale_line_ids = sale_line_obj.search([
            ('moves', 'in', ids),
            ])

        res = super(Move, self).delete(ids)

        if sale_line_ids:
            for sale_line in sale_line_obj.browse(sale_line_ids):
                sale_ids.add(sale_line.sale.id)
            if sale_ids:
                sale_obj.workflow_trigger_validate(list(sale_ids),
                        'shipment_update')
        return res

Move()


class Invoice(ModelSQL, ModelView):
    _name = 'account.invoice'

    sale_exception_state = fields.Function(fields.Selection([
        ('', ''),
        ('ignored', 'Ignored'),
        ('recreated', 'Recreated'),
        ], 'Exception State'), 'get_sale_exception_state')

    def __init__(self):
        super(Invoice, self).__init__()
        self._error_messages.update({
            'delete_sale_invoice': 'You can not delete invoices '
                    'that come from a sale!',
            'reset_invoice_sale': 'You cannot reset to draft '
                    'an invoice generated by a sale.',
            })

    def button_draft(self, ids):
        sale_obj = Pool().get('sale.sale')
        sale_ids = sale_obj.search([
            ('invoices', 'in', ids),
            ])

        if sale_ids:
            self.raise_user_error('reset_invoice_sale')

        return super(Invoice, self).button_draft(ids)

    def get_sale_exception_state(self, ids, name):
        sale_obj = Pool().get('sale.sale')
        sale_ids = sale_obj.search([
            ('invoices', 'in', ids),
            ])

        sales = sale_obj.browse(sale_ids)

        recreated_ids = tuple(i.id for p in sales for i in p.invoices_recreated)
        ignored_ids = tuple(i.id for p in sales for i in p.invoices_ignored)

        res = {}.fromkeys(ids, '')
        for invoice in self.browse(ids):
            if invoice.id in recreated_ids:
                res[invoice.id] = 'recreated'
            elif invoice.id in ignored_ids:
                res[invoice.id] = 'ignored'

        return res

    def delete(self, ids):
        if not ids:
            return True
        if isinstance(ids, (int, long)):
            ids = [ids]
        Transaction().cursor.execute('SELECT id FROM sale_invoices_rel '
                'WHERE invoice IN (' + ','.join(('%s',) * len(ids)) + ')',
                ids)
        if Transaction().cursor.fetchone():
            self.raise_user_error('delete_sale_invoice')
        return super(Invoice, self).delete(ids)

Invoice()


class OpenCustomer(Wizard):
    'Open Customers'
    _name = 'sale.open_customer'
    states = {
        'init': {
            'result': {
                'type': 'action',
                'action': '_action_open',
                'state': 'end',
            },
        },
    }

    def _action_open(self, datas):
        pool = Pool()
        model_data_obj = pool.get('ir.model.data')
        act_window_obj = pool.get('ir.action.act_window')
        wizard_obj = pool.get('ir.action.wizard')
        act_window_id = model_data_obj.get_id('party', 'act_party_form')
        res = act_window_obj.read(act_window_id)
        Transaction().cursor.execute("SELECT DISTINCT(party) FROM sale_sale")
        customer_ids = [line[0] for line in Transaction().cursor.fetchall()]
        res['pyson_domain'] = PYSONEncoder().encode(
                [('id', 'in', customer_ids)])

        model_data_ids = model_data_obj.search([
            ('fs_id', '=', 'act_open_customer'),
            ('module', '=', 'sale'),
            ('inherit', '=', False),
            ], limit=1)
        model_data = model_data_obj.browse(model_data_ids[0])
        wizard = wizard_obj.browse(model_data.db_id)

        res['name'] = wizard.name
        return res

OpenCustomer()


class HandleShipmentExceptionAsk(ModelView):
    'Shipment Exception Ask'
    _name = 'sale.handle.shipment.exception.ask'
    _description = __doc__

    recreate_moves = fields.Many2Many(
        'stock.move', None, None, 'Recreate Moves',
        domain=[('id', 'in', Eval('domain_moves'))], depends=['domain_moves'])
    domain_moves = fields.Many2Many(
        'stock.move', None, None, 'Domain Moves')

    def init(self, module_name):
        cursor = Transaction().cursor
        # Migration from 1.2: packing renamed into shipment
        cursor.execute("UPDATE ir_model "
                "SET model = REPLACE(model, 'packing', 'shipment') "
                "WHERE model like '%%packing%%' AND module = %s",
                (module_name,))
        super(HandleShipmentExceptionAsk, self).init(module_name)

    def default_recreate_moves(self):
        return self.default_domain_moves()

    def default_domain_moves(self):
        sale_line_obj = Pool().get('sale.line')
        active_id = Transaction().context.get('active_id')
        if not active_id:
            return []

        line_ids = sale_line_obj.search([
            ('sale', '=', active_id),
            ])
        lines = sale_line_obj.browse(line_ids)

        domain_moves = []
        for line in lines:
            skip_ids = set(x.id for x in line.moves_ignored)
            skip_ids.update(x.id for x in line.moves_recreated)
            for move in line.moves:
                if move.state == 'cancel' and move.id not in skip_ids:
                    domain_moves.append(move.id)

        return domain_moves

HandleShipmentExceptionAsk()

class HandleShipmentException(Wizard):
    'Handle Shipment Exception'
    _name = 'sale.handle.shipment.exception'
    states = {
        'init': {
            'actions': [],
            'result': {
                'type': 'form',
                'object': 'sale.handle.shipment.exception.ask',
                'state': [
                    ('end', 'Cancel', 'tryton-cancel'),
                    ('ok', 'Ok', 'tryton-ok', True),
                ],
            },
        },
        'ok': {
            'result': {
                'type': 'action',
                'action': '_handle_moves',
                'state': 'end',
            },
        },
    }

    def _handle_moves(self, data):
        pool = Pool()
        sale_obj = pool.get('sale.sale')
        sale_line_obj = pool.get('sale.line')
        move_obj = pool.get('stock.move')
        shipment_obj = pool.get('stock.shipment.out')
        to_recreate = data['form']['recreate_moves'][0][1]
        domain_moves = data['form']['domain_moves'][0][1]

        sale = sale_obj.browse(data['id'])

        for line in sale.lines:
            moves_ignored = []
            moves_recreated = []
            skip_ids = set(x.id for x in line.moves_ignored)
            skip_ids.update(x.id for x in line.moves_recreated)
            for move in line.moves:
                if move.id not in domain_moves or move.id in skip_ids:
                    continue
                if move.id in to_recreate:
                    moves_recreated.append(move.id)
                else:
                    moves_ignored.append(move.id)

            sale_line_obj.write(line.id,{
                'moves_ignored': [('add', moves_ignored)],
                'moves_recreated': [('add', moves_recreated)],
                })

        sale_obj.workflow_trigger_validate(data['id'], 'shipment_ok')

HandleShipmentException()


class HandleInvoiceExceptionAsk(ModelView):
    'Invoice Exception Ask'
    _name = 'sale.handle.invoice.exception.ask'
    _description = __doc__

    recreate_invoices = fields.Many2Many(
        'account.invoice', None, None, 'Recreate Invoices',
        domain=[('id', 'in', Eval('domain_invoices'))],
        depends=['domain_invoices'],
        help='The selected invoices will be recreated. '
            'The other ones will be ignored.')
    domain_invoices = fields.Many2Many(
        'account.invoice', None, None, 'Domain Invoices')

    def default_recreate_invoices(self):
        return self.default_domain_invoices()

    def default_domain_invoices(self):
        sale_obj = Pool().get('sale.sale')
        active_id = Transaction().context.get('active_id')
        if not active_id:
            return []

        sale = sale_obj.browse(active_id)
        skip_ids = set(x.id for x in sale.invoices_ignored)
        skip_ids.update(x.id for x in sale.invoices_recreated)
        domain_invoices = []
        for invoice in sale.invoices:
            if invoice.state == 'cancel' and invoice.id not in skip_ids:
                domain_invoices.append(invoice.id)

        return domain_invoices

HandleInvoiceExceptionAsk()


class HandleInvoiceException(Wizard):
    'Handle Invoice Exception'
    _name = 'sale.handle.invoice.exception'
    states = {
        'init': {
            'actions': [],
            'result': {
                'type': 'form',
                'object': 'sale.handle.invoice.exception.ask',
                'state': [
                    ('end', 'Cancel', 'tryton-cancel'),
                    ('ok', 'Ok', 'tryton-ok', True),
                ],
            },
        },
        'ok': {
            'result': {
                'type': 'action',
                'action': '_handle_invoices',
                'state': 'end',
            },
        },
    }

    def _handle_invoices(self, data):
        sale_obj = Pool().get('sale.sale')
        invoice_obj = Pool().get('account.invoice')
        to_recreate = data['form']['recreate_invoices'][0][1]
        domain_invoices = data['form']['domain_invoices'][0][1]

        sale = sale_obj.browse(data['id'])

        skip_ids = set(x.id for x in sale.invoices_ignored)
        skip_ids.update(x.id for x in sale.invoices_recreated)
        invoices_ignored = []
        invoices_recreated = []
        for invoice in sale.invoices:
            if invoice.id not in domain_invoices or invoice.id in skip_ids:
                continue
            if invoice.id in to_recreate:
                invoices_recreated.append(invoice.id)
            else:
                invoices_ignored.append(invoice.id)

        sale_obj.write(sale.id,{
            'invoices_ignored': [('add', invoices_ignored)],
            'invoices_recreated': [('add', invoices_recreated)],
             })

        sale_obj.workflow_trigger_validate(data['id'], 'invoice_ok')

HandleInvoiceException()
