1833 lines
64 KiB
Python
1833 lines
64 KiB
Python
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
#this repository contains the full copyright notices and license terms.
|
|
from __future__ import with_statement
|
|
import copy
|
|
from decimal import Decimal
|
|
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, In, Eval, Get, Or, Not, Equal, Bool, And, \
|
|
PYSONEncoder
|
|
from trytond.transaction import Transaction
|
|
|
|
class Sale(ModelWorkflow, ModelSQL, ModelView):
|
|
'Sale'
|
|
_name = 'sale.sale'
|
|
_rec_name = 'reference'
|
|
_description = __doc__
|
|
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
states={
|
|
'readonly': Or(Not(Equal(Eval('state'), 'draft')),
|
|
Bool(Eval('lines'))),
|
|
}, domain=[
|
|
('id', If(In('company', Eval('context', {})), '=', '!='),
|
|
Get(Eval('context', {}), 'company', 0)),
|
|
])
|
|
reference = fields.Char('Reference', readonly=True, select=1)
|
|
description = fields.Char('Description',
|
|
states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
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': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
payment_term = fields.Many2One('account.invoice.payment_term',
|
|
'Payment Term', required=True, states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
party = fields.Many2One('party.party', 'Party', change_default=True,
|
|
required=True, states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
}, on_change=['party', 'payment_term'])
|
|
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': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
shipment_address = fields.Many2One('party.address', 'Shipment Address',
|
|
domain=[('party', '=', Eval('party'))], states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
warehouse = fields.Many2One('stock.location', 'Warehouse',
|
|
domain=[('type', '=', 'warehouse')], required=True, states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
currency = fields.Many2One('currency.currency', 'Currency', required=True,
|
|
states={
|
|
'readonly': Or(Not(Equal(Eval('state'), 'draft')),
|
|
And(Bool(Eval('lines')), Bool(Eval('currency')))),
|
|
})
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits',
|
|
on_change_with=['currency']), 'get_function_fields')
|
|
lines = fields.One2Many('sale.line', 'sale', 'Lines', states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
}, on_change=['lines', 'currency', 'party'])
|
|
comment = fields.Text('Comment')
|
|
untaxed_amount = fields.Function(fields.Numeric('Untaxed',
|
|
digits=(16, Eval('currency_digits', 2))), 'get_function_fields')
|
|
tax_amount = fields.Function(fields.Numeric('Tax',
|
|
digits=(16, Eval('currency_digits', 2))), 'get_function_fields')
|
|
total_amount = fields.Function(fields.Numeric('Total',
|
|
digits=(16, Eval('currency_digits', 2))), 'get_function_fields')
|
|
invoice_method = fields.Selection([
|
|
('manual', 'Manual'),
|
|
('order', 'On Order Confirmed'),
|
|
('shipment', 'On Shipment Sent'),
|
|
], 'Invoice Method', required=True, states={
|
|
'readonly': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
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': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
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 = self.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 = self.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 = self.pool.get('ir.date')
|
|
return date_obj.today()
|
|
|
|
def default_currency(self):
|
|
company_obj = self.pool.get('company.company')
|
|
currency_obj = self.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 = self.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):
|
|
return 'order'
|
|
|
|
def default_invoice_state(self):
|
|
return 'none'
|
|
|
|
def default_shipment_method(self):
|
|
return 'order'
|
|
|
|
def default_shipment_state(self):
|
|
return 'none'
|
|
|
|
def on_change_party(self, vals):
|
|
party_obj = self.pool.get('party.party')
|
|
address_obj = self.pool.get('party.address')
|
|
payment_term_obj = self.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 = self.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 = self.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 = self.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):
|
|
currency_obj = self.pool.get('currency.currency')
|
|
tax_obj = self.pool.get('account.tax')
|
|
invoice_obj = self.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 = self.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
|
|
'''
|
|
currency_obj = self.pool.get('currency.currency')
|
|
tax_obj = self.pool.get('account.tax')
|
|
invoice_obj = self.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 = self.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 = self.pool.get('ir.sequence')
|
|
config_obj = self.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 _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 invoice line as key
|
|
and a list of invoice lines values as value
|
|
'''
|
|
line_obj = self.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 = self.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
|
|
'''
|
|
invoice_obj = self.pool.get('account.invoice')
|
|
invoice_line_obj = self.pool.get('account.invoice.line')
|
|
sale_line_obj = self.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_id in invoice_lines:
|
|
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 = self.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 create_shipment(self, sale_id):
|
|
'''
|
|
Create a shipment for the sale
|
|
|
|
:param sale_id: the sale id
|
|
|
|
:return: the created shipment id or None
|
|
'''
|
|
shipment_obj = self.pool.get('stock.shipment.out')
|
|
move_obj = self.pool.get('stock.move')
|
|
sale_line_obj = self.pool.get('sale.line')
|
|
|
|
sale = self.browse(sale_id)
|
|
|
|
moves = self._get_move_sale_line(sale)
|
|
if not moves:
|
|
return
|
|
|
|
with Transaction().set_user(0, set_context=True):
|
|
shipment_id = shipment_obj.create({
|
|
'planned_date': sale.sale_date,
|
|
'customer': sale.party.id,
|
|
'delivery_address': sale.shipment_address.id,
|
|
'reference': sale.reference,
|
|
'warehouse': sale.warehouse.id,
|
|
})
|
|
|
|
for line_id in moves:
|
|
vals = moves[line_id]
|
|
vals['shipment_out'] = shipment_id
|
|
move_id = move_obj.create(vals)
|
|
sale_line_obj.write(line_id, {
|
|
'moves': [('add', move_id)],
|
|
})
|
|
shipment_obj.workflow_trigger_validate(shipment_id, 'waiting')
|
|
return shipment_id
|
|
|
|
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': Not(Equal(Eval('type'), 'line')),
|
|
'required': Equal(Eval('type'), 'line'),
|
|
'readonly': Not(Bool(Eval('_parent_sale'))),
|
|
}, on_change=['product', 'quantity', 'unit',
|
|
'_parent_sale.currency', '_parent_sale.party'])
|
|
unit = fields.Many2One('product.uom', 'Unit',
|
|
states={
|
|
'required': Bool(Eval('product')),
|
|
'invisible': Not(Equal(Eval('type'), 'line')),
|
|
'readonly': Not(Bool(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'])
|
|
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': Not(Equal(Eval('type'), 'line')),
|
|
'readonly': Not(Bool(Eval('_parent_sale'))),
|
|
}, on_change=['product', 'unit', 'quantity', 'description',
|
|
'_parent_sale.party', '_parent_sale.currency'],
|
|
context={
|
|
'locations': If(Bool(Get(Eval('_parent_sale', {}),
|
|
'warehouse')),
|
|
[Get(Eval('_parent_sale', {}), 'warehouse')],
|
|
[]),
|
|
'stock_date_end': Get(Eval('_parent_sale', {}), 'sale_date'),
|
|
'salable': True,
|
|
'stock_skip_warehouse': True,
|
|
})
|
|
unit_price = fields.Numeric('Unit Price', digits=(16, 4),
|
|
states={
|
|
'invisible': Not(Equal(Eval('type'), 'line')),
|
|
'required': Equal(Eval('type'), 'line'),
|
|
})
|
|
amount = fields.Function(fields.Numeric('Amount',
|
|
digits=(16, Get(Eval('_parent_sale', {}), 'currency_digits', 2)),
|
|
states={
|
|
'invisible': Not(In(Eval('type'), ['line', 'subtotal'])),
|
|
'readonly': Not(Bool(Eval('_parent_sale'))),
|
|
}, on_change_with=['type', 'quantity', 'unit_price',
|
|
'_parent_sale.currency']), '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': Not(Equal(Eval('type'), 'line')),
|
|
})
|
|
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, select=1)
|
|
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 = self.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 = self.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('unit'):
|
|
context['uom'] = vals['unit']
|
|
else:
|
|
context['uom'] = product.sale_uom.id
|
|
return context
|
|
|
|
def on_change_product(self, vals):
|
|
party_obj = self.pool.get('party.party')
|
|
product_obj = self.pool.get('product.product')
|
|
uom_obj = self.pool.get('product.uom')
|
|
tax_rule_obj = self.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]
|
|
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 = self.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']]
|
|
return res
|
|
|
|
def on_change_unit(self, vals):
|
|
return self.on_change_quantity(vals)
|
|
|
|
def on_change_with_amount(self, vals):
|
|
currency_obj = self.pool.get('currency.currency')
|
|
if vals.get('type') == 'line':
|
|
if isinstance(vals.get('_parent_sale.currency'), (int, long)):
|
|
currency = currency_obj.browse(vals['_parent_sale.currency'])
|
|
else:
|
|
currency = 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 = self.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 = self.pool.get('product.uom')
|
|
property_obj = self.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':
|
|
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.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 None
|
|
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 = self.pool.get('product.uom')
|
|
|
|
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'] = line.sale.sale_date
|
|
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': Not(Bool(Eval('active'))),
|
|
})
|
|
sale_uom = fields.Many2One('product.uom', 'Sale UOM', states={
|
|
'readonly': Not(Bool(Eval('active'))),
|
|
'invisible': Not(Bool(Eval('salable'))),
|
|
'required': Bool(Eval('salable')),
|
|
}, domain=[
|
|
('category', '=', (Eval('default_uom'), 'uom.category')),
|
|
],
|
|
context={'category': (Eval('default_uom'), 'uom.category')},
|
|
on_change_with=['default_uom', 'sale_uom', 'salable'])
|
|
|
|
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 = And(Not(Bool(Eval('account_category'))),
|
|
Bool(Eval('salable')))
|
|
if not self.account_revenue.states.get('required'):
|
|
self.account_revenue.states['required'] = required
|
|
else:
|
|
self.account_revenue.states['required'] = \
|
|
Or(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 = self.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
|
|
'''
|
|
uom_obj = self.pool.get('product.uom')
|
|
user_obj = self.pool.get('res.user')
|
|
currency_obj = self.pool.get('currency.currency')
|
|
|
|
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:
|
|
res[product.id] = currency_obj.compute(
|
|
user2.company.currency, res[product.id], currency)
|
|
return res
|
|
|
|
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 = self.pool.get('sale.sale')
|
|
sale_line_obj = self.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': Not(Equal(Eval('state'), 'draft')),
|
|
})
|
|
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 = self.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 = self.pool.get('sale.sale')
|
|
sale_line_obj = self.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 = self.pool.get('sale.sale')
|
|
sale_line_obj = self.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 = self.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 = self.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):
|
|
model_data_obj = self.pool.get('ir.model.data')
|
|
act_window_obj = self.pool.get('ir.action.act_window')
|
|
wizard_obj = self.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 = self.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):
|
|
sale_obj = self.pool.get('sale.sale')
|
|
sale_line_obj = self.pool.get('sale.line')
|
|
move_obj = self.pool.get('stock.move')
|
|
shipment_obj = self.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 = self.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 = self.pool.get('sale.sale')
|
|
invoice_obj = self.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()
|