[MERGE] forward port branch 11.0 up to 263b388c50
This commit is contained in:
@@ -52,7 +52,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
arg_list = (tuple(move_state), tuple(account_type))
|
||||
#build the reconciliation clause to see what partner needs to be printed
|
||||
reconciliation_clause = '(l.reconciled IS FALSE)'
|
||||
cr.execute('SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where create_date > %s', (date_from,))
|
||||
cr.execute('SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where max_date > %s', (date_from,))
|
||||
reconciled_after_date = []
|
||||
for row in cr.fetchall():
|
||||
reconciled_after_date += [row[0], row[1]]
|
||||
|
||||
@@ -891,6 +891,83 @@ class TestReconciliation(AccountingTestCase):
|
||||
_move_revert_test_pair(payment_move, reverted_payment_move)
|
||||
_move_revert_test_pair(exchange_move, reverted_exchange_move)
|
||||
|
||||
def test_aged_report_future_payment(self):
|
||||
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
|
||||
account_type = ['receivable']
|
||||
partner = self.env['res.partner'].create({'name': 'AgedPartner'})
|
||||
currency = self.env.user.company_id.currency_id
|
||||
|
||||
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
|
||||
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE'})
|
||||
|
||||
statement = self.make_payment(invoice, journal, 50)
|
||||
|
||||
# Force the payment recording to take place on the invoice date
|
||||
# Although the payment due_date is in the future relative to the invoice
|
||||
# Also, in this case, there can be only 1 partial_reconcile
|
||||
statement_partial_id = statement.move_line_ids.mapped(lambda l: l.matched_credit_ids + l.matched_debit_ids)
|
||||
self.env.cr.execute('UPDATE account_partial_reconcile SET create_date = %(date)s WHERE id = %(partial_id)s',
|
||||
{'date': invoice.date_invoice + ' 00:00:00',
|
||||
'partial_id': statement_partial_id.id})
|
||||
|
||||
# Case 1: report date is invoice date
|
||||
# There should be an entry for the partner
|
||||
report_date_to = invoice.date_invoice
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [{
|
||||
'name': 'AgedPartner',
|
||||
'trust': 'normal',
|
||||
'partner_id': partner.id,
|
||||
'0': 0.0,
|
||||
'1': 0.0,
|
||||
'2': 0.0,
|
||||
'3': 0.0,
|
||||
'4': 0.0,
|
||||
'total': 50.0,
|
||||
'direction': 50.0,
|
||||
}], 'We should have a line in the report for the partner')
|
||||
self.assertEqual(len(amls[partner.id]), 1, 'We should have 1 account move lines for the partner')
|
||||
|
||||
positive_line = [line for line in amls[partner.id] if line['line'].balance > 0]
|
||||
|
||||
self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50')
|
||||
|
||||
# Case 2: report date between invoice date and payment date
|
||||
# There should be an entry for the partner
|
||||
# And the amount has shifted to '1-30 due'
|
||||
report_date_to = time.strftime('%Y') + '-07-08'
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [{
|
||||
'name': 'AgedPartner',
|
||||
'trust': 'normal',
|
||||
'partner_id': partner.id,
|
||||
'0': 0.0,
|
||||
'1': 0.0,
|
||||
'2': 0.0,
|
||||
'3': 0.0,
|
||||
'4': 50.0,
|
||||
'total': 50.0,
|
||||
'direction': 0.0,
|
||||
}], 'We should have a line in the report for the partner')
|
||||
self.assertEqual(len(amls[partner.id]), 1, 'We should have 1 account move lines for the partner')
|
||||
|
||||
positive_line = [line for line in amls[partner.id] if line['line'].balance > 0]
|
||||
|
||||
self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50')
|
||||
|
||||
# Case 2: report date on payment date
|
||||
# There should not be an entry for the partner
|
||||
report_date_to = time.strftime('%Y') + '-07-15'
|
||||
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
|
||||
|
||||
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
|
||||
self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point')
|
||||
self.assertFalse(amls.get(partner.id, False), 'The aged receivable should not have amls either')
|
||||
|
||||
def test_partial_reconcile_currencies_02(self):
|
||||
####
|
||||
# Day 1: Invoice Cust/001 to customer (expressed in USD)
|
||||
|
||||
@@ -1372,7 +1372,8 @@ class Meeting(models.Model):
|
||||
rrule_type=False,
|
||||
rrule='',
|
||||
recurrency=False,
|
||||
final_date=datetime.strptime(data.get('start'), DEFAULT_SERVER_DATETIME_FORMAT if data['allday'] else DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=values.get('duration', False) or data.get('duration'))
|
||||
final_date=False,
|
||||
end_type=False
|
||||
)
|
||||
|
||||
# do not copy the id
|
||||
|
||||
@@ -294,7 +294,7 @@ class Message(models.Model):
|
||||
'id': attachment['id'],
|
||||
'filename': attachment['datas_fname'],
|
||||
'name': attachment['name'],
|
||||
'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'],
|
||||
'mimetype': 'application/octet-stream' if safari and attachment['mimetype'] and 'video' in attachment['mimetype'] else attachment['mimetype'],
|
||||
}) for attachment in attachments_data)
|
||||
|
||||
# 3. Tracking values
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<filter string="Today Activities" name="activities_today"
|
||||
domain="[('date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<separator/>
|
||||
<group expand="0" string="Group By">
|
||||
|
||||
@@ -52,7 +52,7 @@ class MrpProduction(models.Model):
|
||||
domain=[('type', 'in', ['product', 'consu'])],
|
||||
readonly=True, required=True,
|
||||
states={'confirmed': [('readonly', False)]})
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id')
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id', readonly=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity To Produce',
|
||||
default=1.0, digits=dp.get_precision('Product Unit of Measure'),
|
||||
|
||||
@@ -193,13 +193,15 @@ class PosOrder(models.Model):
|
||||
values['partner_id'],
|
||||
(values['product_id'], tuple(values['tax_ids'][0][2]), values['name']),
|
||||
values['analytic_account_id'],
|
||||
values['debit'] > 0)
|
||||
values['debit'] > 0,
|
||||
values.get('currency_id'))
|
||||
elif data_type == 'tax':
|
||||
order_id = values.pop('order_id', False)
|
||||
tax_key = ('tax',
|
||||
values['partner_id'],
|
||||
values['tax_line_id'],
|
||||
values['debit'] > 0)
|
||||
values['debit'] > 0,
|
||||
values.get('currency_id'))
|
||||
if options.get('rounding_method') == 'round_globally':
|
||||
tax_key = ('tax',
|
||||
values['tax_line_id'],
|
||||
@@ -209,7 +211,8 @@ class PosOrder(models.Model):
|
||||
return ('counter_part',
|
||||
values['partner_id'],
|
||||
values['account_id'],
|
||||
values['debit'] > 0)
|
||||
values['debit'] > 0,
|
||||
values.get('currency_id'))
|
||||
return False
|
||||
|
||||
def _action_create_invoice_line(self, line=False, invoice_id=False):
|
||||
@@ -328,6 +331,8 @@ class PosOrder(models.Model):
|
||||
current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
|
||||
current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
|
||||
current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
|
||||
if 'currency_id' in values:
|
||||
current_value['amount_currency'] = current_value.get('amount_currency', 0.0) + values.get('amount_currency', 0.0)
|
||||
if key[0] == 'tax' and rounding_method == 'round_globally':
|
||||
if current_value['debit'] - current_value['credit'] > 0:
|
||||
current_value['debit'] = current_value['debit'] - current_value['credit']
|
||||
@@ -347,8 +352,14 @@ class PosOrder(models.Model):
|
||||
assert order.lines, _('The POS order must have lines when calling this method')
|
||||
# Create an move for each order line
|
||||
cur = order.pricelist_id.currency_id
|
||||
cur_company = order.company_id.currency_id
|
||||
amount_cur_company = 0.0
|
||||
date_order = (order.date_order or fields.Datetime.now())[:10]
|
||||
for line in order.lines:
|
||||
amount = line.price_subtotal
|
||||
if cur != cur_company:
|
||||
amount_subtotal = cur.with_context(date=date_order).compute(line.price_subtotal, cur_company)
|
||||
else:
|
||||
amount_subtotal = line.price_subtotal
|
||||
|
||||
# Search for the income account
|
||||
if line.product_id.property_account_income_id.id:
|
||||
@@ -369,17 +380,22 @@ class PosOrder(models.Model):
|
||||
# Just like for invoices, a group of taxes must be present on this base line
|
||||
# As well as its children
|
||||
base_line_tax_ids = _flatten_tax_and_children(line.tax_ids_after_fiscal_position).filtered(lambda tax: tax.type_tax_use in ['sale', 'none'])
|
||||
insert_data('product', {
|
||||
data = {
|
||||
'name': name,
|
||||
'quantity': line.qty,
|
||||
'product_id': line.product_id.id,
|
||||
'account_id': income_account,
|
||||
'analytic_account_id': self._prepare_analytic_account(line),
|
||||
'credit': ((amount > 0) and amount) or 0.0,
|
||||
'debit': ((amount < 0) and -amount) or 0.0,
|
||||
'credit': ((amount_subtotal > 0) and amount_subtotal) or 0.0,
|
||||
'debit': ((amount_subtotal < 0) and -amount_subtotal) or 0.0,
|
||||
'tax_ids': [(6, 0, base_line_tax_ids.ids)],
|
||||
'partner_id': partner_id
|
||||
})
|
||||
}
|
||||
if cur != cur_company:
|
||||
data['currency_id'] = cur.id
|
||||
data['amount_currency'] = -abs(line.price_subtotal) if data.get('credit') else abs(line.price_subtotal)
|
||||
amount_cur_company += data['credit'] - data['debit']
|
||||
insert_data('product', data)
|
||||
|
||||
# Create the tax lines
|
||||
taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id)
|
||||
@@ -387,34 +403,58 @@ class PosOrder(models.Model):
|
||||
continue
|
||||
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
for tax in taxes.compute_all(price, cur, line.qty)['taxes']:
|
||||
insert_data('tax', {
|
||||
if cur != cur_company:
|
||||
round_tax = False if rounding_method == 'round_globally' else True
|
||||
amount_tax = cur.with_context(date=date_order).compute(tax['amount'], cur_company, round=round_tax)
|
||||
else:
|
||||
amount_tax = tax['amount']
|
||||
data = {
|
||||
'name': _('Tax') + ' ' + tax['name'],
|
||||
'product_id': line.product_id.id,
|
||||
'quantity': line.qty,
|
||||
'account_id': tax['account_id'] or income_account,
|
||||
'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0,
|
||||
'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0,
|
||||
'credit': ((amount_tax > 0) and amount_tax) or 0.0,
|
||||
'debit': ((amount_tax < 0) and -amount_tax) or 0.0,
|
||||
'tax_line_id': tax['id'],
|
||||
'partner_id': partner_id,
|
||||
'order_id': order.id
|
||||
})
|
||||
}
|
||||
if cur != cur_company:
|
||||
data['currency_id'] = cur.id
|
||||
data['amount_currency'] = -abs(tax['amount']) if data.get('credit') else abs(tax['amount'])
|
||||
amount_cur_company += data['credit'] - data['debit']
|
||||
insert_data('tax', data)
|
||||
|
||||
# round tax lines per order
|
||||
if rounding_method == 'round_globally':
|
||||
for group_key, group_value in grouped_data.items():
|
||||
if group_key[0] == 'tax':
|
||||
for line in group_value:
|
||||
line['credit'] = cur.round(line['credit'])
|
||||
line['debit'] = cur.round(line['debit'])
|
||||
line['credit'] = cur_company.round(line['credit'])
|
||||
line['debit'] = cur_company.round(line['debit'])
|
||||
if line.get('currency_id'):
|
||||
line['amount_currency'] = cur.round(line.get('amount_currency', 0.0))
|
||||
|
||||
# counterpart
|
||||
insert_data('counter_part', {
|
||||
if cur != cur_company:
|
||||
# 'amount_cur_company' contains the sum of the AML converted in the company
|
||||
# currency. This makes the logic consistent with 'compute_invoice_totals' from
|
||||
# 'account.invoice'. It ensures that the counterpart line is the same amount than
|
||||
# the sum of the product and taxes lines.
|
||||
amount_total = amount_cur_company
|
||||
else:
|
||||
amount_total = order.amount_total
|
||||
data = {
|
||||
'name': _("Trade Receivables"), # order.name,
|
||||
'account_id': order_account,
|
||||
'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
|
||||
'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
|
||||
'credit': ((amount_total < 0) and -amount_total) or 0.0,
|
||||
'debit': ((amount_total > 0) and amount_total) or 0.0,
|
||||
'partner_id': partner_id
|
||||
})
|
||||
}
|
||||
if cur != cur_company:
|
||||
data['currency_id'] = cur.id
|
||||
data['amount_currency'] = -abs(order.amount_total) if data.get('credit') else abs(order.amount_total)
|
||||
insert_data('counter_part', data)
|
||||
|
||||
order.write({'state': 'done', 'account_move': move.id})
|
||||
|
||||
|
||||
@@ -188,6 +188,10 @@ class TestUi(odoo.tests.HttpCase):
|
||||
one_week_from_now = today + timedelta(weeks=1)
|
||||
two_weeks_from_now = today + timedelta(weeks=2)
|
||||
|
||||
public_pricelist = env['product.pricelist'].create({
|
||||
'name': 'Public Pricelist',
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Dates',
|
||||
'item_ids': [(0, 0, {
|
||||
@@ -274,9 +278,19 @@ class TestUi(odoo.tests.HttpCase):
|
||||
'type': 'cash',
|
||||
'company_id': main_company.id,
|
||||
'journal_user': True})],
|
||||
'pricelist_id': public_pricelist.id,
|
||||
'available_pricelist_ids': [(4, pricelist.id) for pricelist in all_pricelists],
|
||||
})
|
||||
|
||||
# Change the default sale pricelist of customers,
|
||||
# so the js tests can expect deterministically this pricelist when selecting a customer.
|
||||
field = env['ir.model.fields']._get('res.partner', 'property_product_pricelist')
|
||||
env['ir.property'].search([
|
||||
('name', '=', 'property_product_pricelist'),
|
||||
('fields_id', '=', field.id),
|
||||
('res_id', '=', False)
|
||||
]).write({'value_reference': 'product.pricelist,%s' % public_pricelist.id})
|
||||
|
||||
# open a session, the /pos/web controller will redirect to it
|
||||
main_pos_config.open_session_cb()
|
||||
|
||||
|
||||
@@ -602,3 +602,123 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
# I close the rescue session
|
||||
rescue_session.action_pos_session_closing_control()
|
||||
self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed")
|
||||
|
||||
def test_order_to_payment_currency(self):
|
||||
"""
|
||||
In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing.
|
||||
I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price.
|
||||
The order will be in a different currency than the company currency.
|
||||
"""
|
||||
# Make sure the company is in USD
|
||||
self.env.cr.execute(
|
||||
"UPDATE res_company SET currency_id = %s WHERE id = %s",
|
||||
[self.env.ref('base.USD').id, self.env.user.company_id.id])
|
||||
|
||||
# Demo data are crappy, clean-up the rates
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2010-01-01',
|
||||
'rate': 2.0,
|
||||
'currency_id': self.env.ref('base.EUR').id,
|
||||
})
|
||||
|
||||
# I click on create a new session button
|
||||
self.pos_config.open_session_cb()
|
||||
|
||||
# I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl)
|
||||
# and 3 units of PCSC349 at 300 EUR. (Tax Excl)
|
||||
self.pos_order_pos0 = self.PosOrder.create({
|
||||
'company_id': self.company_id,
|
||||
'pricelist_id': self.partner1.property_product_pricelist.copy(default={'currency_id': self.env.ref('base.EUR').id}).id,
|
||||
'partner_id': self.partner1.id,
|
||||
'lines': [(0, 0, {
|
||||
'name': "OL/0001",
|
||||
'product_id': self.product3.id,
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': 2.0,
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0002",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': 3.0,
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I check that the total of the order is equal to 450*2 + 300*3*1.05
|
||||
# and the tax of the order is equal to 900 -(450 * 2 / 1.1) +
|
||||
# 300*0.05*3
|
||||
self.assertLess(
|
||||
abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05)),
|
||||
0.01, 'The order has a wrong amount, tax included.')
|
||||
|
||||
self.assertLess(
|
||||
abs(self.pos_order_pos0.amount_tax - (900 - (450 * 2 / 1.1) + 300 * 0.05 * 3)),
|
||||
0.01, 'The order has a wrong tax amount.')
|
||||
|
||||
# I want to add a global discount of 5 percent using the wizard
|
||||
|
||||
self.pos_discount_0 = self.env['pos.discount'].create({'discount': 5.0})
|
||||
|
||||
context = {"active_model": "pos.order", "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
|
||||
|
||||
# I click the apply button to set the discount on all lines
|
||||
self.pos_discount_0.with_context(context).apply_discount()
|
||||
|
||||
# I check that the total of the order is now equal to (450*2 +
|
||||
# 300*3*1.05)*0.95
|
||||
self.assertLess(
|
||||
abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05) * 0.95),
|
||||
0.01, 'The order has a wrong total including tax and discounts')
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with a
|
||||
# partial amount of 100.0 EUR
|
||||
context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
|
||||
self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': 100.0
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
context_payment = {'active_id': self.pos_order_pos0.id}
|
||||
self.pos_make_payment_0.with_context(context_payment).check()
|
||||
|
||||
# I check that the order is not marked as paid yet
|
||||
self.assertEqual(self.pos_order_pos0.state, 'draft', 'Order should be in draft state.')
|
||||
|
||||
# On the second payment proposition, I check that it proposes me the
|
||||
# remaining balance which is 1790.0 EUR
|
||||
defs = self.pos_make_payment_0.with_context({'active_id': self.pos_order_pos0.id}).default_get(['amount'])
|
||||
|
||||
self.assertLess(
|
||||
abs(defs['amount'] - ((450 * 2 + 300 * 3 * 1.05) * 0.95 - 100.0)), 0.01, "The remaining balance is incorrect.")
|
||||
|
||||
#'I pay the remaining balance.
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
|
||||
|
||||
self.pos_make_payment_1 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': (450 * 2 + 300 * 3 * 1.05) * 0.95 - 100.0
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
self.pos_make_payment_1.with_context(context_make_payment).check()
|
||||
|
||||
# I check that the order is marked as paid
|
||||
self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.')
|
||||
|
||||
# I generate the journal entries
|
||||
self.pos_order_pos0._create_account_move_line()
|
||||
|
||||
# I test that the generated journal entry is attached to the PoS order
|
||||
self.assertTrue(self.pos_order_pos0.account_move, "Journal entry has not been attached to Pos order.")
|
||||
|
||||
# Check the amounts
|
||||
debit_lines = self.pos_order_pos0.account_move.mapped('line_ids.debit')
|
||||
credit_lines = self.pos_order_pos0.account_move.mapped('line_ids.credit')
|
||||
amount_currency_lines = self.pos_order_pos0.account_move.mapped('line_ids.amount_currency')
|
||||
self.assertEqual(set(debit_lines), {876.39, 0.0})
|
||||
self.assertEqual(set(credit_lines), {0.0, 21.38, 427.5, 38.87, 388.64})
|
||||
self.assertEqual(set(amount_currency_lines), {1752.75, -42.75, -855.0, -77.73, -777.27})
|
||||
|
||||
@@ -522,7 +522,7 @@
|
||||
<group>
|
||||
<group string="Vendor">
|
||||
<field name="product_variant_count" invisible="1"/>
|
||||
<field name="product_id" groups="product.group_product_variant" domain="[('product_tmpl_id', '=', product_tmpl_id)]"/>
|
||||
<field name="product_id" groups="product.group_product_variant" domain="[('product_tmpl_id', '=', product_tmpl_id)]" options="{'no_create_edit': True}"/>
|
||||
<field name="name" context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}"/>
|
||||
<field name="product_name"/>
|
||||
<field name="product_code"/>
|
||||
|
||||
@@ -520,7 +520,6 @@
|
||||
<field name="sequence"/>
|
||||
<field name="date_deadline"/>
|
||||
<field name="message_needaction_counter"/>
|
||||
<field name="attachment_ids"/>
|
||||
<field name="displayed_image_id"/>
|
||||
<field name="active"/>
|
||||
<field name="legend_blocked"/>
|
||||
|
||||
@@ -442,7 +442,7 @@ class ResourceCalendar(models.Model):
|
||||
|
||||
for day in rrule.rrule(rrule.DAILY,
|
||||
dtstart=start_dt,
|
||||
until=end_dt,
|
||||
until=end_dt.replace(hour=23, minute=59, second=59, microsecond=999999),
|
||||
byweekday=self._get_weekdays()):
|
||||
start_time = datetime.time.min
|
||||
if day.date() == start_dt.date():
|
||||
@@ -472,7 +472,7 @@ class ResourceCalendar(models.Model):
|
||||
|
||||
for day in rrule.rrule(rrule.DAILY,
|
||||
dtstart=start_dt,
|
||||
until=end_dt,
|
||||
until=end_dt.replace(hour=23, minute=59, second=59, microsecond=999999),
|
||||
byweekday=self._get_weekdays()):
|
||||
start_time = datetime.time.min
|
||||
if day.date() == start_dt.date():
|
||||
|
||||
@@ -233,6 +233,15 @@ class ResourceWorkingHours(TestResourceCommon):
|
||||
compute_leaves=False)
|
||||
self.assertEqual(res, 40.0)
|
||||
|
||||
def test_calendar_working_hours_count(self):
|
||||
calendar = self.env.ref('resource.resource_calendar_std_35h')
|
||||
res = calendar.get_work_hours_count(
|
||||
Datetime.from_string('2017-05-03 14:03:00'), # Wednesday (8:00-12:00, 13:00-16:00)
|
||||
Datetime.from_string('2017-05-04 11:03:00'), # Thursday (8:00-12:00, 13:00-16:00)
|
||||
resource_id=None,
|
||||
compute_leaves=False)
|
||||
self.assertEqual(res, 5.0)
|
||||
|
||||
def test_calendar_working_hours_leaves(self):
|
||||
# new API: resource and leaves
|
||||
# res: 2 weeks -> 40 hours - (3+4) leave hours
|
||||
|
||||
@@ -585,6 +585,7 @@ class StockMove(models.Model):
|
||||
moves_to_merge = []
|
||||
for candidate_moves in candidate_moves_list:
|
||||
# First step find move to merge.
|
||||
candidate_moves = candidate_moves.with_context(prefetch_fields=False)
|
||||
for k, g in groupby(sorted(candidate_moves, key=self._prepare_merge_move_sort_method), key=itemgetter(*distinct_fields)):
|
||||
moves = self.env['stock.move'].concat(*g).filtered(lambda m: m.state not in ('done', 'cancel', 'draft'))
|
||||
# If we have multiple records we will merge then in a single one.
|
||||
|
||||
@@ -21,7 +21,7 @@ class StockMoveLine(models.Model):
|
||||
help='The stock operation where the packing has been made')
|
||||
move_id = fields.Many2one(
|
||||
'stock.move', 'Stock Move',
|
||||
help="Change to a better name")
|
||||
help="Change to a better name", index=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True)
|
||||
product_qty = fields.Float(
|
||||
|
||||
@@ -457,7 +457,7 @@ class StockMove(models.Model):
|
||||
# force the amount in the context, but in the case it is 0 it'll create an entry
|
||||
# for the entire cost of the move. This case happens when the candidates moves
|
||||
# entirely compensate the problematic move.
|
||||
if not corrected_value:
|
||||
if move.company_id.currency_id.is_zero(corrected_value):
|
||||
continue
|
||||
|
||||
if move._is_in():
|
||||
|
||||
@@ -341,7 +341,8 @@ var BooleanField = SelectionField.extend(/** @lends instance.web.search.BooleanF
|
||||
*/
|
||||
var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
value_from: function (facetValue) {
|
||||
return time.date_to_str(facetValue.get('value'));
|
||||
var serverFormat = (this.attrs && this.attrs.type === 'datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
return facetValue.get('valueAsMoment').clone().format(serverFormat);
|
||||
},
|
||||
complete: function (needle) {
|
||||
// Make sure the needle has a correct format before the creation of the moment object. See
|
||||
@@ -355,6 +356,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
}
|
||||
|
||||
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
|
||||
|
||||
if (!m.isValid()) { return $.when(null); }
|
||||
var date_string = field_utils.format[t](m, {type: t});
|
||||
var label = _.str.sprintf(_.str.escapeHTML(
|
||||
@@ -366,7 +368,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: date_string, value: m.toDate()}]
|
||||
values: [{label: date_string, value: m.toDate(), valueAsMoment: m}]
|
||||
}
|
||||
}]);
|
||||
}
|
||||
@@ -383,11 +385,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
* @class
|
||||
* @extends instance.web.DateField
|
||||
*/
|
||||
var DateTimeField = DateField.extend(/** @lends instance.web.search.DateTimeField# */{
|
||||
value_from: function (facetValue) {
|
||||
return time.datetime_to_str(facetValue.get('value'));
|
||||
}
|
||||
});
|
||||
var DateTimeField = DateField;
|
||||
|
||||
var ManyToOneField = CharField.extend({
|
||||
default_operator: {},
|
||||
|
||||
@@ -100,9 +100,17 @@ return Widget.extend({
|
||||
var controllerContext;
|
||||
this.trigger_up('get_controller_context', {
|
||||
callback: function (ctx) {
|
||||
controllerContext = ctx;
|
||||
if (ctx && ctx.orderedBy) {
|
||||
var sort = _.map(ctx.orderedBy, function (obj) {
|
||||
var order = obj.asc ? 'ASC' : 'DESC';
|
||||
return obj.name + ' ' + order;
|
||||
});
|
||||
self.searchview.dataset.set_sort(sort);
|
||||
}
|
||||
controllerContext = _.omit(ctx, ['orderedBy']);
|
||||
},
|
||||
});
|
||||
|
||||
var results = pyeval.eval_domains_and_contexts({
|
||||
domains: search.domains,
|
||||
contexts: [user_context].concat(search.contexts.concat(controllerContext || [])),
|
||||
@@ -182,7 +190,18 @@ return Widget.extend({
|
||||
category: _t("Custom Filter"),
|
||||
icon: 'fa-star',
|
||||
field: {
|
||||
get_context: function () { return filter.context; },
|
||||
get_context: function () {
|
||||
var sortParsed = JSON.parse(filter.sort || "[]");
|
||||
var orderedBy = [];
|
||||
_.each(sortParsed, function (sort) {
|
||||
orderedBy.push({
|
||||
name: sort[0] === '-' ? sort.slice(1) : sort,
|
||||
asc: sort[0] === '-' ? false : true,
|
||||
});
|
||||
});
|
||||
|
||||
return _.defaults({}, filter.context, {orderedBy : orderedBy});
|
||||
},
|
||||
get_groupby: function () { return [filter.context]; },
|
||||
get_domain: function () { return filter.domain; }
|
||||
},
|
||||
|
||||
@@ -1456,6 +1456,11 @@ var BasicModel = AbstractModel.extend({
|
||||
record._rawChanges[name] = val;
|
||||
return;
|
||||
}
|
||||
if (record._rawChanges[name]) {
|
||||
// if previous _rawChanges exists, clear them since the field is now knwon
|
||||
// and restoring outdated onchange over posterious change is wrong
|
||||
delete record._rawChanges[name];
|
||||
}
|
||||
var oldValue = name in record._changes ? record._changes[name] : record.data[name];
|
||||
var id;
|
||||
if (field.type === 'many2one') {
|
||||
@@ -2570,11 +2575,6 @@ var BasicModel = AbstractModel.extend({
|
||||
relationField: field.relation_field,
|
||||
viewType: view ? view.type : fieldInfo.viewType,
|
||||
});
|
||||
// set existing changes to the list
|
||||
if (record._changes && record._changes[fieldName]) {
|
||||
list._changes = self.localData[record._changes[fieldName]]._changes;
|
||||
record._changes[fieldName] = list.id;
|
||||
}
|
||||
record.data[fieldName] = list.id;
|
||||
if (!fieldInfo.__no_fetch) {
|
||||
var def = self._readUngroupedList(list).then(function () {
|
||||
@@ -3365,7 +3365,7 @@ var BasicModel = AbstractModel.extend({
|
||||
model: params.modelName,
|
||||
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
|
||||
openGroupByDefault: params.openGroupByDefault,
|
||||
orderedBy: params.orderedBy || [],
|
||||
orderedBy: params.orderedBy || (params.context && params.context.orderedBy) || [],
|
||||
orderedResIDs: params.orderedResIDs,
|
||||
parentID: params.parentID,
|
||||
rawContext: params.rawContext,
|
||||
@@ -4053,6 +4053,7 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
if (options.context !== undefined) {
|
||||
element.context = options.context;
|
||||
element.orderedBy = options.context.orderedBy || element.orderedBy;
|
||||
}
|
||||
if (options.domain !== undefined) {
|
||||
element.domain = options.domain;
|
||||
|
||||
@@ -77,6 +77,14 @@ var ListController = BasicController.extend({
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
/*
|
||||
* @override
|
||||
*/
|
||||
getContext: function () {
|
||||
var context = this._super.apply(this, arguments);
|
||||
context.orderedBy = this.model.get(this.handle, {raw: true}).orderedBy || [];
|
||||
return context;
|
||||
},
|
||||
/**
|
||||
* Returns the list of currently selected res_ids (with the check boxes on
|
||||
* the left)
|
||||
@@ -187,6 +195,7 @@ var ListController = BasicController.extend({
|
||||
} else {
|
||||
this.selectedRecords = [];
|
||||
}
|
||||
|
||||
params.selectedRecords = this.selectedRecords;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -360,13 +360,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
:not(.o_row) > .o_field_widget,
|
||||
:not(.o_row):not(.o_data_cell) > .o_field_widget,
|
||||
.o_row > .o_field_widget:last-child { // Note: this does not take care
|
||||
// of an invisible last-child but
|
||||
// it does not really matter
|
||||
// Makes extra buttons (e.g. m2o external button) overflow on the
|
||||
// right padding of the parent element
|
||||
.o_input_dropdown {
|
||||
> .o_input_dropdown {
|
||||
.o-flex(1, 0, auto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9875,6 +9875,95 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange on unloaded record clearing posterious change', function (assert) {
|
||||
// when we got onchange result for fields of record that were not
|
||||
// already available because they were in a inline view not already
|
||||
// opened, in a given configuration the change were applied ignoring
|
||||
// posteriously changed data, thus an added/removed/modified line could
|
||||
// be reset to the original onchange data
|
||||
assert.expect(5);
|
||||
|
||||
var numUserOnchange = 0;
|
||||
|
||||
this.data.user.onchanges = {
|
||||
partner_ids: function (obj) {
|
||||
// simulate actual server onchange after save of modal with new record
|
||||
if (numUserOnchange === 0) {
|
||||
obj.partner_ids = _.clone(obj.partner_ids);
|
||||
obj.partner_ids.unshift([5]);
|
||||
obj.partner_ids[1][2].turtles.unshift([5]);
|
||||
obj.partner_ids[2] = [1, 2, {
|
||||
display_name: 'second record',
|
||||
trululu: 1,
|
||||
turtles: [[5]],
|
||||
}];
|
||||
} else if (numUserOnchange === 1) {
|
||||
obj.partner_ids = _.clone(obj.partner_ids);
|
||||
obj.partner_ids.unshift([5]);
|
||||
obj.partner_ids[1][2].turtles.unshift([5]);
|
||||
obj.partner_ids[2][2].turtles.unshift([5]);
|
||||
}
|
||||
numUserOnchange++;
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'user',
|
||||
data: this.data,
|
||||
arch: '<form><sheet><group>' +
|
||||
'<field name="partner_ids">' +
|
||||
'<form>'+
|
||||
'<field name="trululu"/>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>' +
|
||||
'<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group></sheet></form>',
|
||||
res_id: 17,
|
||||
});
|
||||
|
||||
// open first partner and change turtle name
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_data_row:eq(0)').click();
|
||||
$('.modal .o_data_cell:eq(0)').click();
|
||||
$('.modal input[name="display_name"]').val('Donatello').change();
|
||||
$('.modal .btn-primary').click();
|
||||
|
||||
// open second partner and add a turtle
|
||||
form.$('.o_data_row:eq(1)').click();
|
||||
$('.modal .o_field_x2many_list_row_add a').click();
|
||||
$('.modal input[name="display_name"]').val('Michelangelo').change();
|
||||
$('.modal .btn-primary').click();
|
||||
|
||||
assert.strictEqual(numUserOnchange, 2,
|
||||
'there should 2 and only 2 onchange from closing the partner modal');
|
||||
|
||||
// check first record still has change
|
||||
form.$('.o_data_row:eq(0)').click();
|
||||
assert.strictEqual($('.modal .o_data_row').length, 1,
|
||||
'only 1 turtle for first partner');
|
||||
assert.strictEqual($('.modal .o_data_row').text(), 'Donatello',
|
||||
'first partner turtle is Donatello');
|
||||
$('.modal .o_form_button_cancel').click();
|
||||
|
||||
// check second record still has changes
|
||||
form.$('.o_data_row:eq(1)').click();
|
||||
assert.strictEqual($('.modal .o_data_row').length, 1,
|
||||
'only 1 turtle for second partner');
|
||||
assert.strictEqual($('.modal .o_data_row').text(), 'Michelangelo',
|
||||
'second partner turtle is Michelangelo');
|
||||
$('.modal .o_form_button_cancel').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on URL should not open the record', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -309,6 +309,99 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('ordered list, sort attribute in context', function (assert) {
|
||||
assert.expect(1);
|
||||
// Equivalent to saving a custom filter
|
||||
|
||||
this.data.foo.fields.foo.sortable = true;
|
||||
this.data.foo.fields.date.sortable = true;
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="date"/>' +
|
||||
'</tree>',
|
||||
});
|
||||
|
||||
// Descending order on Foo
|
||||
list.$('th.o_column_sortable:contains("Foo")').click();
|
||||
list.$('th.o_column_sortable:contains("Foo")').click();
|
||||
|
||||
// Ascending order on Date
|
||||
list.$('th.o_column_sortable:contains("Date")').click();
|
||||
|
||||
var listContext = list.getContext();
|
||||
assert.deepEqual(listContext,
|
||||
{
|
||||
orderedBy: [{
|
||||
name: 'date',
|
||||
asc: true,
|
||||
}, {
|
||||
name: 'foo',
|
||||
asc: false,
|
||||
}]
|
||||
}, 'the list should have the right orderedBy in context');
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Loading a filter with a sort attribute', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.foo.fields.foo.sortable = true;
|
||||
this.data.foo.fields.date.sortable = true;
|
||||
|
||||
var searchReads = 0;
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="date"/>' +
|
||||
'</tree>',
|
||||
context: {
|
||||
orderedBy: [{
|
||||
name: 'date',
|
||||
asc: true,
|
||||
}, {
|
||||
name: 'foo',
|
||||
asc: false,
|
||||
}]
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
if (searchReads === 0) {
|
||||
assert.strictEqual(args.sort, 'date ASC, foo DESC',
|
||||
'The sort attribute of the filter should be used by the initial search_read');
|
||||
} else if (searchReads === 1) {
|
||||
assert.strictEqual(args.sort, 'date DESC, foo ASC',
|
||||
'The sort attribute of the filter should be used by the next search_read');
|
||||
}
|
||||
searchReads += 1;
|
||||
}
|
||||
return this._super.apply(this,arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// Simulate loading a filter
|
||||
list.update({
|
||||
context: {
|
||||
orderedBy: [{
|
||||
name: 'date',
|
||||
asc: false,
|
||||
}, {
|
||||
name: 'foo',
|
||||
asc: true,
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
list.destroy()
|
||||
});
|
||||
|
||||
QUnit.test('many2one field rendering', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ define([
|
||||
(keycode > 185 && keycode < 193) || // ;=,-./` (in order)
|
||||
(keycode > 218 && keycode < 223))) { // [\]' (in order))
|
||||
eventName = 'visible';
|
||||
} else if (!keycode) {
|
||||
} else if (!keycode && event.key !== 'Dead') {
|
||||
self.invoke('restoreRange', $editable);
|
||||
}
|
||||
// ODOO: end_modification)
|
||||
|
||||
@@ -64,7 +64,7 @@ class SaleOrder(models.Model):
|
||||
('date_order', '<=', abandoned_datetime),
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', '=', 'draft'),
|
||||
('partner_id.id', '!=', self.env.ref('base.public_partner').id),
|
||||
('partner_id', '!=', self.env.ref('base.public_partner').id),
|
||||
('order_line', '!=', False)
|
||||
])
|
||||
# is_abandoned domain possibilities
|
||||
|
||||
@@ -36,6 +36,10 @@ class WebsiteSaleDigital(CustomerPortal):
|
||||
order = response.qcontext['order']
|
||||
invoiced_lines = request.env['account.invoice.line'].sudo().search([('invoice_id', 'in', order.invoice_ids.ids), ('invoice_id.state', '=', 'paid')])
|
||||
products = invoiced_lines.mapped('product_id') | order.order_line.filtered(lambda r: not r.price_subtotal).mapped('product_id')
|
||||
if not order.amount_total:
|
||||
# in that case, we should add all download links to the products
|
||||
# since there is nothing to pay, so we shouldn't wait for an invoice
|
||||
products = order.order_line.mapped('product_id')
|
||||
|
||||
purchased_products_attachments = {}
|
||||
for product in products:
|
||||
|
||||
@@ -551,7 +551,7 @@ state_ae_fu,ae,"Fujairah","FU"
|
||||
state_ae_rk,ae,"Ras al-Khaimah","RK"
|
||||
state_ae_sh,ae,"Sharjah","SH"
|
||||
state_ae_uq,ae,"Umm al-Quwain","UQ"
|
||||
state_ar_c,ar,"Buenos Aires City","C"
|
||||
state_ar_c,ar,"Ciudad Autónoma de Buenos Aires","C"
|
||||
state_ar_b,ar,"Buenos Aires","B"
|
||||
state_ar_k,ar,"Catamarca","K"
|
||||
state_ar_h,ar,"Chaco","H"
|
||||
|
||||
|
@@ -44,6 +44,7 @@ def _get_wkhtmltopdf_bin():
|
||||
|
||||
# Check the presence of Wkhtmltopdf and return its version at Odoo start-up
|
||||
wkhtmltopdf_state = 'install'
|
||||
wkhtmltopdf_dpi_zoom_ratio = False
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
[_get_wkhtmltopdf_bin(), '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
@@ -61,6 +62,8 @@ else:
|
||||
wkhtmltopdf_state = 'upgrade'
|
||||
else:
|
||||
wkhtmltopdf_state = 'ok'
|
||||
if LooseVersion(version) >= LooseVersion('0.12.2'):
|
||||
wkhtmltopdf_dpi_zoom_ratio = True
|
||||
|
||||
if config['workers'] == 1:
|
||||
_logger.info('You need to start Odoo with at least two workers to print a pdf version of the reports.')
|
||||
@@ -237,14 +240,19 @@ class IrActionsReport(models.Model):
|
||||
else:
|
||||
command_args.extend(['--margin-top', str(paperformat_id.margin_top)])
|
||||
|
||||
dpi = None
|
||||
if specific_paperformat_args and specific_paperformat_args.get('data-report-dpi'):
|
||||
command_args.extend(['--dpi', str(specific_paperformat_args['data-report-dpi'])])
|
||||
dpi = int(specific_paperformat_args['data-report-dpi'])
|
||||
elif paperformat_id.dpi:
|
||||
if os.name == 'nt' and int(paperformat_id.dpi) <= 95:
|
||||
_logger.info("Generating PDF on Windows platform require DPI >= 96. Using 96 instead.")
|
||||
command_args.extend(['--dpi', '96'])
|
||||
dpi = 96
|
||||
else:
|
||||
command_args.extend(['--dpi', str(paperformat_id.dpi)])
|
||||
dpi = paperformat_id.dpi
|
||||
if dpi:
|
||||
command_args.extend(['--dpi', str(dpi)])
|
||||
if wkhtmltopdf_dpi_zoom_ratio:
|
||||
command_args.extend(['--zoom', str(96.0 / dpi)])
|
||||
|
||||
if specific_paperformat_args and specific_paperformat_args.get('data-report-header-spacing'):
|
||||
command_args.extend(['--header-spacing', str(specific_paperformat_args['data-report-header-spacing'])])
|
||||
|
||||
@@ -20,6 +20,18 @@ TYPE2FIELD = {
|
||||
'selection': 'value_text',
|
||||
}
|
||||
|
||||
TYPE2CLEAN = {
|
||||
'boolean': bool,
|
||||
'integer': lambda val: val or False,
|
||||
'float': lambda val: val or False,
|
||||
'char': lambda val: val or False,
|
||||
'text': lambda val: val or False,
|
||||
'selection': lambda val: val or False,
|
||||
'binary': lambda val: val or False,
|
||||
'date': lambda val: val or False,
|
||||
'datetime': lambda val: val or False,
|
||||
}
|
||||
|
||||
|
||||
class Property(models.Model):
|
||||
_name = 'ir.property'
|
||||
@@ -147,48 +159,61 @@ class Property(models.Model):
|
||||
if not ids:
|
||||
return {}
|
||||
|
||||
domain = self._get_domain(name, model)
|
||||
if domain is None:
|
||||
return dict.fromkeys(ids, False)
|
||||
|
||||
# retrieve the values for the given ids and the default value, too
|
||||
refs = {('%s,%s' % (model, id)): id for id in ids}
|
||||
refs[False] = False
|
||||
domain += [('res_id', 'in', list(refs))]
|
||||
|
||||
# note: order by 'company_id asc' will return non-null values first
|
||||
props = self.search(domain, order='company_id asc')
|
||||
result = {}
|
||||
|
||||
field = self.env[model]._fields[name]
|
||||
field_id = self.env['ir.model.fields']._get(model, name).id
|
||||
company_id = (
|
||||
self._context.get('force_company')
|
||||
or self.env['res.company']._company_default_get(model, field_id).id
|
||||
)
|
||||
|
||||
if field.type == 'many2one':
|
||||
# optimization for many2one fields
|
||||
Comodel = self.env[field.comodel_name]
|
||||
co_ids = set()
|
||||
for prop in props:
|
||||
# for a given res_id, take the first property only
|
||||
id = refs.pop(prop.res_id, None)
|
||||
if id is not None:
|
||||
val = prop.value_reference
|
||||
if val:
|
||||
val = int(val.split(',')[1])
|
||||
co_ids.add(val)
|
||||
result[id] = val
|
||||
# check for existence in batch, and update result accordingly
|
||||
existing = {rec.id: rec for rec in Comodel.browse(co_ids).exists()}
|
||||
result = {id: existing.get(val, Comodel) for id, val in result.items()}
|
||||
comodel = self.env[field.comodel_name]
|
||||
model_pos = len(model) + 2
|
||||
value_pos = len(comodel._name) + 2
|
||||
# retrieve values: both p.res_id and p.value_reference are formatted
|
||||
# as "<rec._name>,<rec.id>"; the purpose of the LEFT JOIN is to
|
||||
# return the value id if it exists, NULL otherwise
|
||||
query = """
|
||||
SELECT substr(p.res_id, %s)::integer, r.id
|
||||
FROM ir_property p
|
||||
LEFT JOIN {} r ON substr(p.value_reference, %s)::integer=r.id
|
||||
WHERE p.fields_id=%s
|
||||
AND (p.company_id=%s OR p.company_id IS NULL)
|
||||
AND (p.res_id IN %s OR p.res_id IS NULL)
|
||||
ORDER BY p.company_id NULLS FIRST
|
||||
""".format(comodel._table)
|
||||
params = [model_pos, value_pos, field_id, company_id]
|
||||
clean = comodel.browse
|
||||
|
||||
elif field.type in TYPE2FIELD:
|
||||
model_pos = len(model) + 2
|
||||
# retrieve values: p.res_id is formatted as "<rec._name>,<rec.id>"
|
||||
query = """
|
||||
SELECT substr(p.res_id, %s)::integer, p.{}
|
||||
FROM ir_property p
|
||||
WHERE p.fields_id=%s
|
||||
AND (p.company_id=%s OR p.company_id IS NULL)
|
||||
AND (p.res_id IN %s OR p.res_id IS NULL)
|
||||
ORDER BY p.company_id NULLS FIRST
|
||||
""".format(TYPE2FIELD[field.type])
|
||||
params = [model_pos, field_id, company_id]
|
||||
clean = TYPE2CLEAN[field.type]
|
||||
|
||||
else:
|
||||
for prop in props:
|
||||
# for a given res_id, take the first property only
|
||||
id = refs.pop(prop.res_id, None)
|
||||
if id is not None:
|
||||
result[id] = prop.get_by_record()
|
||||
return dict.fromkeys(ids, False)
|
||||
|
||||
# set the default value to the ids that are not in result
|
||||
default_value = result.pop(False, False)
|
||||
# retrieve values
|
||||
cr = self.env.cr
|
||||
result = {}
|
||||
refs = {"%s,%s" % (model, id) for id in ids}
|
||||
for sub_refs in cr.split_for_in_conditions(refs):
|
||||
cr.execute(query, params + [sub_refs])
|
||||
result.update(cr.fetchall())
|
||||
|
||||
# remove default value, add missing values, and format them
|
||||
default = result.pop(None, None)
|
||||
for id in ids:
|
||||
result.setdefault(id, default_value)
|
||||
result[id] = clean(result.get(id, default))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -389,6 +389,8 @@ class CompanyDependent(models.Model):
|
||||
_name = 'test_new_api.company'
|
||||
|
||||
foo = fields.Char(company_dependent=True)
|
||||
date = fields.Date(company_dependent=True)
|
||||
moment = fields.Datetime(company_dependent=True)
|
||||
tag_id = fields.Many2one('test_new_api.multi.tag', company_dependent=True)
|
||||
|
||||
|
||||
|
||||
@@ -670,20 +670,42 @@ class TestFields(common.TransactionCase):
|
||||
'value': tag0, 'type': 'many2one'})
|
||||
|
||||
# create/modify a record, and check the value for each user
|
||||
record = self.env['test_new_api.company'].create({'foo': 'main', 'tag_id': tag1})
|
||||
record = self.env['test_new_api.company'].create({
|
||||
'foo': 'main',
|
||||
'date': '1932-11-09',
|
||||
'moment': '1932-11-09 00:00:00',
|
||||
'tag_id': tag1.id,
|
||||
})
|
||||
record.invalidate_cache()
|
||||
self.assertEqual(record.sudo(user0).foo, 'main')
|
||||
self.assertEqual(record.sudo(user1).foo, 'default')
|
||||
self.assertEqual(record.sudo(user2).foo, 'default')
|
||||
self.assertEqual(record.sudo(user0).date, '1932-11-09')
|
||||
self.assertEqual(record.sudo(user1).date, False)
|
||||
self.assertEqual(record.sudo(user2).date, False)
|
||||
self.assertEqual(record.sudo(user0).moment, '1932-11-09 00:00:00')
|
||||
self.assertEqual(record.sudo(user1).moment, False)
|
||||
self.assertEqual(record.sudo(user2).moment, False)
|
||||
self.assertEqual(record.sudo(user0).tag_id, tag1)
|
||||
self.assertEqual(record.sudo(user1).tag_id, tag0)
|
||||
self.assertEqual(record.sudo(user2).tag_id, tag0)
|
||||
|
||||
record.sudo(user1).write({'foo': 'alpha', 'tag_id': tag2.id})
|
||||
record.sudo(user1).write({
|
||||
'foo': 'alpha',
|
||||
'date': '1932-12-10',
|
||||
'moment': '1932-12-10 23:59:59',
|
||||
'tag_id': tag2.id,
|
||||
})
|
||||
record.invalidate_cache()
|
||||
self.assertEqual(record.sudo(user0).foo, 'main')
|
||||
self.assertEqual(record.sudo(user1).foo, 'alpha')
|
||||
self.assertEqual(record.sudo(user2).foo, 'default')
|
||||
self.assertEqual(record.sudo(user0).date, '1932-11-09')
|
||||
self.assertEqual(record.sudo(user1).date, '1932-12-10')
|
||||
self.assertEqual(record.sudo(user2).date, False)
|
||||
self.assertEqual(record.sudo(user0).moment, '1932-11-09 00:00:00')
|
||||
self.assertEqual(record.sudo(user1).moment, '1932-12-10 23:59:59')
|
||||
self.assertEqual(record.sudo(user2).moment, False)
|
||||
self.assertEqual(record.sudo(user0).tag_id, tag1)
|
||||
self.assertEqual(record.sudo(user1).tag_id, tag2)
|
||||
self.assertEqual(record.sudo(user2).tag_id, tag0)
|
||||
|
||||
@@ -578,6 +578,7 @@ class JsonRequest(WebRequest):
|
||||
super(JsonRequest, self).__init__(*args)
|
||||
|
||||
self.jsonp_handler = None
|
||||
self.params = {}
|
||||
|
||||
args = self.httprequest.args
|
||||
jsonp = args.get('jsonp')
|
||||
|
||||
+15
-9
@@ -205,16 +205,22 @@ def log_ormcache_stats(sig=None, frame=None):
|
||||
|
||||
me = threading.currentThread()
|
||||
me_dbname = getattr(me, 'dbname', 'n/a')
|
||||
entries = defaultdict(int)
|
||||
for dbname, reg in Registry.registries.items():
|
||||
for key in reg.cache:
|
||||
entries[(dbname,) + key[:2]] += 1
|
||||
for key, count in sorted(entries.items()):
|
||||
dbname, model_name, method = key
|
||||
|
||||
for dbname, reg in sorted(Registry.registries.items()):
|
||||
# set logger prefix to dbname
|
||||
me.dbname = dbname
|
||||
stat = STAT[key]
|
||||
_logger.info("%6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s",
|
||||
count, stat.hit, stat.miss, stat.err, stat.ratio, model_name, method.__name__)
|
||||
entries = defaultdict(int)
|
||||
# beware: we use .keys() on purpose here (reg.cache is not a real dict)
|
||||
for key in reg.cache.keys():
|
||||
entries[key[:2]] += 1
|
||||
# show entries sorted by model name, method name
|
||||
for key in sorted(entries, key=lambda key: (key[0], key[1].__name__)):
|
||||
model, method = key
|
||||
stat = STAT[(dbname, model, method)]
|
||||
_logger.info(
|
||||
"%6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s",
|
||||
entries[key], stat.hit, stat.miss, stat.err, stat.ratio, model, method.__name__,
|
||||
)
|
||||
|
||||
me.dbname = me_dbname
|
||||
|
||||
|
||||
Reference in New Issue
Block a user