[MERGE] forward port branch saas-17 up to 48b2ce60ed

This commit is contained in:
Christophe Simonis
2017-08-10 18:07:47 +02:00
108 changed files with 1622 additions and 403 deletions
@@ -307,7 +307,10 @@ class AccountBankStatement(models.Model):
#try to assign partner to bank_statement_line
stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
if st_lines_left and stl_to_assign_partner and refs:
if st_lines_left and stl_to_assign_partner and refs\
and st_lines_left[0].journal_id.default_credit_account_id\
and st_lines_left[0].journal_id.default_debit_account_id:
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
FROM account_move_line aml
JOIN account_account acc ON acc.id = aml.account_id
@@ -605,13 +608,14 @@ class AccountBankStatementLine(models.Model):
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
select_clause = "SELECT aml.id "
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
AND aml.payment_id IS NOT NULL)
OR
("""+acc_type+""" AND aml.reconciled = false)
)"""
account_clause = ''
if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id:
account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR"
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
""" + account_clause + """
("""+acc_type+""" AND aml.reconciled = false)
)"""
where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause
where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause
if split:
+3 -3
View File
@@ -177,7 +177,7 @@ class AccountInvoice(models.Model):
if payment_currency_id and payment_currency_id == self.currency_id:
amount_to_show = amount_currency
else:
amount_to_show = payment.company_id.currency_id.with_context(date=payment.date).compute(amount,
amount_to_show = payment.company_id.currency_id.with_context(date=self.date).compute(amount,
self.currency_id)
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
@@ -880,7 +880,6 @@ class AccountInvoice(models.Model):
if tax.amount_type == "group":
for child_tax in tax.children_tax_ids:
done_taxes.append(child_tax.id)
done_taxes.append(tax.id)
res.append({
'invoice_tax_line_id': tax_line.id,
'tax_line_id': tax_line.tax_id.id,
@@ -892,8 +891,9 @@ class AccountInvoice(models.Model):
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
'invoice_id': self.id,
'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else []
'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else []
})
done_taxes.append(tax.id)
return res
def inv_line_characteristic_hashcode(self, invoice_line):
+24 -20
View File
@@ -540,6 +540,7 @@ class AccountMoveLine(models.Model):
{2}
WHERE
a.reconcile IS TRUE
AND l.full_reconcile_id is NULL
{3}
{4}
{5}
@@ -1628,30 +1629,15 @@ class AccountPartialReconcile(models.Model):
}
return self.env['account.move'].create(move_vals)
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
def _compute_partial_lines(self):
if self._context.get('skip_full_reconcile_check'):
#when running the manual reconciliation wizard, don't check the partials separately for full
#reconciliation or exchange rate because it is handled manually after the whole processing
return res
return self
#check if the reconcilation is full
#first, gather all journal items involved in the reconciliation just created
partial_rec_set = OrderedDict.fromkeys([x for x in res])
aml_set = self.env['account.move.line']
partial_rec_set = OrderedDict.fromkeys([x for x in self])
aml_set = aml_to_balance = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
@@ -1659,7 +1645,7 @@ class AccountPartialReconcile(models.Model):
#possible to compute the exchange difference entry and it has to be done manually.
currency = list(partial_rec_set)[0].currency_id
maxdate = '0000-00-00'
aml_to_balance = self.env['account.move.line']
for partial_rec in partial_rec_set:
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
@@ -1703,6 +1689,24 @@ class AccountPartialReconcile(models.Model):
'reconciled_line_ids': [(6, 0, aml_ids)],
'exchange_move_id': exchange_move_id,
})
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
res._compute_partial_lines()
return res
@api.multi
+1 -1
View File
@@ -91,7 +91,7 @@ class account_abstract_payment(models.AbstractModel):
total = 0
for inv in self.invoice_ids:
if inv.currency_id == payment_currency:
total += inv.residual_company_signed
total += inv.residual_signed
else:
total += inv.company_currency_id.with_context(date=self.payment_date).compute(
inv.residual_company_signed, payment_currency)
+144
View File
@@ -477,3 +477,147 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.credit, 2), line['credit'])
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
def test_partial_reconcile_currencies(self):
# client Account (payable, rsa)
# Debit Credit
# --------------------------------------------------------
# Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
# Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
# Pay c: 25/0.8 = 31.25 |
#
# Debit_currency = 100 | Credit currency = 100
# Debit = 147.91 | Credit = 166.66
# Balance Debit = 18.75
# Counterpart Credit goes in Exchange diff
dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
# Setting up rates for USD (main_company is in EUR)
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
'rate': 0.5,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
'rate': 0.75,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
'rate': 0.80,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
# Preparing Invoices (from vendor)
invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_a.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_b.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_a.action_invoice_open()
invoice_b.action_invoice_open()
# Preparing Payments
# One partial for invoice_a (fully assigned to it)
payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# One that will complete the payment of a, the rest goes to b
payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 50,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# The last one will complete the payment of b
payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
payment_a.post()
payment_b.post()
payment_c.post()
# Assigning payments to invoices
debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
invoice_a.assign_outstanding_credit(debit_line_a.id)
invoice_a.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_c.id)
# Asserting correctness (only in the payable account)
full_reconcile = False
for inv in (invoice_a + invoice_b):
self.assertTrue(inv.reconciled)
for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
self.assertEqual(aml.amount_residual, 0.0)
self.assertEqual(aml.amount_residual_currency, 0.0)
self.assertTrue(aml.reconciled)
if not full_reconcile:
full_reconcile = aml.full_reconcile_id
else:
self.assertTrue(aml.full_reconcile_id == full_reconcile)
full_rec_move = full_reconcile.exchange_move_id
# Globally check whether the amount is correct
self.assertEqual(full_rec_move.amount, 18.75)
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
@@ -16,7 +16,7 @@
<form string="Configure Accounting" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
@@ -108,3 +108,6 @@ class AccountInvoiceLine(models.Model):
elif invoice.type == 'in_invoice':
self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
super(AccountInvoiceLine, self)._set_additional_fields(invoice)
def get_invoice_line_account(self, type, product, fpos, company):
return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
@@ -125,7 +125,7 @@ QUnit.test('basic rendering', function (assert) {
});
QUnit.test('click events are correctly triggered', function (assert) {
assert.expect(3);
assert.expect(2);
var form = createView({
View: FormView,
@@ -146,8 +146,7 @@ QUnit.test('click events are correctly triggered', function (assert) {
intercepts: {
execute_action: function (event) {
var data = event.data;
assert.strictEqual(data.model, 'line', "should have correct model");
assert.deepEqual(data.res_ids, [4], "should have correct res_ids");
assert.strictEqual(data.env.model, 'line', "should have correct model");
assert.strictEqual(data.action_data.name, 'create_move',
"should call correct method");
},
@@ -7,7 +7,7 @@
<form string="General Settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div id="business_documents">
+6 -5
View File
@@ -13,9 +13,10 @@ class StockQuantPackage(models.Model):
@api.one
@api.depends('quant_ids')
def _compute_weight(self):
weight = 0
for quant in self.quant_ids:
weight += quant.qty * quant.product_id.weight
smls = self.env['stock.move.line'].search([('product_id', '!=', False), ('result_package_id', '=', self.id)])
weight = 0.0
for sml in smls:
weight += sml.product_uom_id._compute_quantity(sml.qty_done, sml.product_id.uom_id) * sml.product_id.weight
self.weight = weight
weight = fields.Float(compute='_compute_weight')
@@ -60,7 +61,7 @@ class StockPicking(models.Model):
self.ensure_one()
packs = set()
for move_line in self.move_line_ids:
if move_line.result_package_id and move_line.result_package_id.packaging_id:
if move_line.result_package_id:
packs.add(move_line.result_package_id.id)
self.package_ids = list(packs)
@@ -70,7 +71,7 @@ class StockPicking(models.Model):
weight = 0.0
for move_line in self.move_line_ids:
if move_line.product_id and not move_line.result_package_id:
weight += move_line.product_uom_id._compute_quantity(move_line.product_qty, move_line.product_id.uom_id) * move_line.product_id.weight
weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight
self.weight_bulk = weight
@api.one
+3
View File
@@ -316,6 +316,9 @@ class EventEvent(models.Model):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
@api.multi
def _is_event_registrable(self):
return True
class EventRegistration(models.Model):
_name = 'event.registration'
@@ -8,7 +8,7 @@
<form String="Event settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Advertising</h2>
+6
View File
@@ -53,6 +53,12 @@ class Event(models.Model):
})
for ticket in self.event_type_id.event_ticket_ids]
@api.multi
def _is_event_registrable(self):
self.ensure_one()
if not self.event_ticket_ids:
return True
return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active))
class EventTicket(models.Model):
_name = 'event.event.ticket'
+11 -10
View File
@@ -98,7 +98,7 @@
</table>
% else:
<span style="color:#A8A8A8;font-size:13px;">
The following message contains the current progress for the challenge <strong>${object.name}</strong>.
Challenge: <strong>${object.name}</strong>.
</span>
% for line in challenge_lines:
<!-- Header + Button table -->
@@ -113,7 +113,7 @@
% if len(line['goals'])>2:
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:10px;">
<tr><td style="padding:0 30px;">
<table cellspacing="0" cellpadding="0" width="100%">
<table cellspacing="0" cellpadding="0" width="100%" style="table-layout: fixed;">
<tr>
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]]
% for goal in top_goals:
@@ -144,9 +144,10 @@
<div style="height:${heightA}px;">
${extra_div | safe}
<div style="height:55px;">
<img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="/web/image?model=res.users&amp;field=image_small&amp;id=${goal['user_id']}" alt=""/>
% set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small
<img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="data:image/jpeg;base64,${path}" alt="${goal['name']}"/>
</div>
<div align="center" style ="color:${bgColor};">
<div align="center" style ="color:${bgColor};height:20px">
${goal['name']}
</div
</div>
@@ -177,8 +178,8 @@
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:30px;margin-bottom:5px;color:#666666;">
<thead>
<tr style="color:#9A6C8E; font-size:12px;">
<th style="width:10%;text-align:center;">Rank</th>
<th style="width:15%;text-align:left;">Name</th>
<th style="width:15%;text-align:center;">Rank</th>
<th style="width:25%;text-align:left;">Name</th>
<th style="width:30%;text-align:right;">Performance
% if line['suffix']:
(${line['suffix']})
@@ -199,12 +200,12 @@
% set tdColor = 'gray'
% set mutedColor = '#AAAAAA'
% set tdPercentageColor = '#9A6C8E'
<td width="10%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
<td width="15%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
</td>
<td width="15%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
<td width="20%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
<td width="25%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
<td width="30%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
</td>
<td width="20%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
<td width="30%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#DADADA;"></td>
@@ -7,7 +7,7 @@
<form string="Configuration" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Check-In/Out</h2>
@@ -64,4 +64,4 @@
<menuitem id="hr_attendance.menu_hr_attendance_settings" name="Configuration" parent="menu_hr_attendance_root"
sequence="99" action="action_hr_attendance_settings" groups="hr_attendance.group_hr_attendance_manager"/>
</odoo>
</odoo>
+1 -1
View File
@@ -385,7 +385,7 @@ class HrExpenseSheet(models.Model):
_order = "accounting_date desc, id desc"
name = fields.Char(string='Expense Report Summary', required=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
state = fields.Selection([('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Posted'),
@@ -9,7 +9,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Recording</h2>
+1 -1
View File
@@ -392,7 +392,7 @@
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
<field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_amount" sum="Total Amount"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
</tree>
@@ -15,7 +15,7 @@
<form string="Configure Payroll" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<field name="module_l10n_fr_hr_payroll" invisible="1"/>
<field name="module_l10n_be_hr_payroll" invisible="1"/>
@@ -8,7 +8,7 @@
<form string="Configure HR Recruitment" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Job Posting</h2>
+1 -1
View File
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def default_get(self, field_list):
result = super(AccountAnalyticLine, self).default_get(field_list)
if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'):
if 'employee_id' in field_list and result.get('user_id'):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
+1
View File
@@ -571,6 +571,7 @@ class MailTemplate(models.Model):
'name': attachment[0],
'datas_fname': attachment[0],
'datas': attachment[1],
'type': 'binary',
'res_model': 'mail.message',
'res_id': mail.mail_message_id.id,
}
+5 -3
View File
@@ -950,13 +950,14 @@ class MailThread(models.AbstractModel):
# Alias: check alias_contact settings
if alias:
obj = None
if thread_id:
obj = record_set[0]
elif alias.alias_parent_thread_id:
elif alias.alias_parent_model_id and alias.alias_parent_thread_id:
obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
elif model and hasattr(record_set, '_alias_check_contact'):
elif model:
obj = self.env[model]
else:
if not hasattr(obj, '_alias_check_contact'):
obj = self.env['mail.alias.mixin']
check_result = obj._alias_check_contact(message, message_dict, alias)
if check_result is not True:
@@ -1727,6 +1728,7 @@ class MailThread(models.AbstractModel):
data_attach = {
'name': name,
'datas': base64.b64encode(str(content)),
'type': 'binary',
'datas_fname': name,
'description': name,
'res_model': message_data['model'],
@@ -7,7 +7,7 @@
<form string="Configure Mass Mailing" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Design</h2>
+1
View File
@@ -8,6 +8,7 @@ from . import mrp_message
from . import mrp_routing
from . import mrp_workcenter
from . import mrp_production
from . import stock_traceability
from . import mrp_unbuild
from . import mrp_workorder
from . import procurement
+27 -15
View File
@@ -89,36 +89,48 @@ class MrpUnbuild(models.Model):
consume_move = self._generate_consume_moves()[0]
produce_moves = self._generate_produce_moves()
if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves):
raise UserError(_('You should specify a manufacturing order in order to find the correct tracked products.'))
if consume_move.has_tracking != 'none':
self.env['stock.move.line'].create({
'move_id': consume_move.id,
'lot_id': self.lot_id.id,
'qty_done': consume_move.product_uom_qty,
'product_qty': consume_move.product_uom_qty,
'product_id': consume_move.product_id.id,
'product_uom_id': consume_move.product_uom.id,
'location_id': consume_move.location_id.id,
'location_dest_id': consume_move.location_dest_id.id,})
'location_dest_id': consume_move.location_dest_id.id,
})
else:
consume_move.quantity_done = consume_move.product_uom_qty
consume_move.action_done()
original_quants = consume_move.quant_ids.mapped('consumed_quant_ids')
#TODO: needs to be replaced by checking the different stock.move.lots
# TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took
for produce_move in produce_moves:
if produce_move.has_tracking != 'none':
original = original_quants.filtered(lambda quant: quant.product_id == produce_move.product_id)
self.env['stock.move.line'].create({
'move_id': produce_move.id,
'lot_id': original.lot_id.id,
'qty_done': produce_move.product_uom_qty,
'product_qty': produce_move.product_uom_qty,
'location_id': produce_move.location_id.id,
'location_dest_id': produce_move.location_dest_id.id,
})
original_move = self.mo_id.move_raw_ids.filtered(lambda move: move.product_id == produce_move.product_id)
needed_quantity = produce_move.product_qty
for move_lines in original_move.mapped('move_line_ids'):
# Iterate over all move_lines until we unbuilded the correct quantity.
taken_quantity = min(needed_quantity, move_lines.qty_done)
if taken_quantity:
self.env['stock.move.line'].create({
'move_id': produce_move.id,
'lot_id': move_lines.lot_id.id,
'qty_done': taken_quantity,
'product_id': produce_move.product_id.id,
'product_uom_id': move_lines.product_uom_id.id,
'location_id': produce_move.location_id.id,
'location_dest_id': produce_move.location_dest_id.id,
})
needed_quantity -= taken_quantity
else:
produce_move.quantity_done = produce_move.product_uom_qty
produce_moves.action_done()
produced_quant_ids = produce_moves.mapped('quant_ids').filtered(lambda quant: quant.qty > 0)
consume_move.quant_ids.sudo().write({'produced_quant_ids': [(6, 0, produced_quant_ids.ids)]})
produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0)
consume_move.move_line_ids.write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]})
return self.write({'state': 'done'})
+2 -1
View File
@@ -203,7 +203,8 @@ class MrpWorkorder(models.Model):
'move_id': move.id,
'product_id': move.product_id.id,
'lot_id': False,
'product_qty': 0.0,
'product_uom_qty': 0.0,
'product_uom_id': move.product_uom.id,
'qty_done': min(1.0, qty_todo),
'workorder_id': self.id,
'done_wo': False,
+2 -2
View File
@@ -131,8 +131,8 @@ class StockMove(models.Model):
@api.multi
def action_confirm(self):
moves = self.env['stock.move']
for move in self:
moves = self
for move in self.filtered(lambda m: m.production_id):
moves |= move.action_explode()
# we go further with the list of ids potentially changed by action_explode
return super(StockMove, moves).action_confirm()
-19
View File
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockQuant(models.Model):
_inherit = 'stock.quant'
consumed_quant_ids = fields.Many2many(
'stock.quant', 'stock_quant_consume_rel', 'produce_quant_id', 'consume_quant_id')
produced_quant_ids = fields.Many2many(
'stock.quant', 'stock_quant_consume_rel', 'consume_quant_id', 'produce_quant_id')
def _prepare_history(self):
vals = super(StockQuant, self)._prepare_history()
vals['consumed_quant_ids'] = [(4, quant.id) for quant in self.consumed_quant_ids]
vals['produced_quant_ids'] = [(4, quant.id) for quant in self.produced_quant_ids]
return vals
+21
View File
@@ -0,0 +1,21 @@
from odoo import models, api
class MrpStockReport(models.TransientModel):
_inherit = 'stock.traceability.report'
@api.model
def get_links(self, move_line):
res_model, res_id, ref = super(MrpStockReport, self).get_links(move_line)
if move_line.production_id:
res_model = 'mrp.production'
res_id = move_line.production_id.id
ref = move_line.production_id.name
if move_line.move_id.unbuild_id:
res_model = 'mrp.unbuild'
res_id = move_line.move_id.unbuild_id.id
ref = move_line.move_id.unbuild_id.name
if move_line.move_id.consume_unbuild_id:
res_model = 'mrp.unbuild'
res_id = move_line.move_id.consume_unbuild_id.id
ref = move_line.move_id.consume_unbuild_id.name
return res_model, res_id, ref
+2 -2
View File
@@ -5,7 +5,7 @@ from odoo import api, models
class MrpBomCost(models.AbstractModel):
_name = 'report.mrp_bom_cost'
_name = 'report.mrp.mrp_bom_cost_report'
@api.multi
def get_lines(self, boms):
@@ -44,4 +44,4 @@ class MrpBomCost(models.AbstractModel):
def get_report_values(self, docids, data=None):
boms = self.env['mrp.bom'].browse(docids)
res = self.get_lines(boms)
return self.env['ir.actions.report'].render_template('mrp.mrp_bom_cost_report', {'lines': res})
return {'lines': res}
@@ -5,7 +5,7 @@ from odoo import api, models
class BomStructureReport(models.AbstractModel):
_name = 'report.mrp.report_mrpbomstructure'
_name = 'report.mrp.mrp_bom_structure_report'
def get_children(self, object, level=0):
result = []
+3 -3
View File
@@ -5,7 +5,7 @@
string="BOM Structure"
id="action_report_bom_structure"
model="mrp.bom"
name="mrp.report_mrpbomstructure"
name="mrp.mrp_bom_structure_report"
file="mrp.report.mrp_bom_templates"
report_type="qweb-pdf"
print_report_name="'BOM - %s' % (object.product_id.name or object.product_tmpl_id.name or '')"
@@ -25,8 +25,8 @@
string="BOM Cost"
id="action_report_bom_price"
model="mrp.bom"
name="mrp_bom_cost"
file="mrp_bom_cost"
name="mrp.mrp_bom_cost_report"
file="mrp.mrp_bom_cost_report"
report_type="qweb-html"
print_report_name="'BOM Cost - %s' % (object.product_id.name or object.product_tmpl_id.name or '')"
/>
+2 -1
View File
@@ -4,4 +4,5 @@ from . import test_bom
from . import test_order
from . import test_stock
from . import test_workorder_operation
from . import test_procurement
from . import test_procurement
from . import test_unbuild
+440
View File
@@ -0,0 +1,440 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.exceptions import UserError
class TestUnbuild(TestMrpCommon):
def setUp(self):
super(TestUnbuild, self).setUp()
self.stock_location = self.env.ref('stock.stock_location_stock')
def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none'):
""" This function generate a manufacturing order with one final product
and two consumed product. Arguments allows to choose the tracking for each
different products.
It returns the MO, used bom, the tree products
"""
product_to_build = self.env['product.product'].create({
'name': 'Young Tom',
'type': 'product',
'tracking': tracking_final,
})
product_to_use_1 = self.env['product.product'].create({
'name': 'Botox',
'type': 'product',
'tracking': tracking_base_1,
})
product_to_use_2 = self.env['product.product'].create({
'name': 'Old Tom',
'type': 'product',
'tracking': tracking_base_2,
})
bom_1 = self.env['mrp.bom'].create({
'product_id': product_to_build.id,
'product_tmpl_id': product_to_build.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': 1}),
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': 4})
]})
mo = self.env['mrp.production'].create({
'name': 'MO 1',
'product_id': product_to_build.id,
'product_uom_id': product_to_build.uom_id.id,
'product_qty': 5.0,
'bom_id': bom_1.id,
})
return mo, bom_1, product_to_build, product_to_use_1, product_to_use_2
def test_unbuild_standart(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. None of the products for this
test are tracked. It checks the stock state after each order
and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo()
self.assertEqual(len(mo), 1, 'MO should have been created')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
})
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 3.0,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 2.0,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 5.0,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
# Check quantity in stock after last unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_final_lot(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. Only the final product is tracked
by lot. It checks the stock state after each order
and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
lot = self.env['stock.production.lot'].create({
'name': 'lot1',
'product_id': p_final.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
'lot_id': lot.id,
})
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
unbuild_order = self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 3.0,
'product_uom_id': self.uom_unit.id,
})
# This should fail since we do not choose a lot to unbuild for final product.
with self.assertRaises(UserError):
unbuild_order.action_unbuild()
unbuild_order.lot_id = lot.id
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 2.0,
'lot_id': lot.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 5.0,
'lot_id': lot.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_comnsumed_lot(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. Only once of the two consumed
product is tracked by lot. It checks the stock state after each
order and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
lot = self.env['stock.production.lot'].create({
'name': 'lot1',
'product_id': p1.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
for ml in mo.move_raw_ids.mapped('move_line_ids'):
ml.qty_done = ml.product_qty
if ml.product_id.tracking != 'none':
self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.')
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
})
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
unbuild_order = self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 3.0,
'product_uom_id': self.uom_unit.id,
})
# This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore)
with self.assertRaises(UserError):
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock')
unbuild_order.mo_id = mo.id
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 2.0,
'mo_id': mo.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 5.0,
'mo_id': mo.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_everything_tracked(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. All the products for this
test are tracked. It checks the stock state after each order
and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_2='lot', tracking_base_1='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
lot_final = self.env['stock.production.lot'].create({
'name': 'lot_final',
'product_id': p_final.id,
})
lot_1 = self.env['stock.production.lot'].create({
'name': 'lot_consumed_1',
'product_id': p1.id,
})
lot_2 = self.env['stock.production.lot'].create({
'name': 'lot_consumed_2',
'product_id': p2.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
mo.action_assign()
for ml in mo.move_raw_ids.mapped('move_line_ids'):
ml.qty_done = ml.product_qty
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
'lot_id': lot_final.id,
})
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 5 product in stock')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
unbuild_order = self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 3.0,
'product_uom_id': self.uom_unit.id,
})
with self.assertRaises(UserError):
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
unbuild_order.mo_id = mo.id
with self.assertRaises(UserError):
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
unbuild_order.lot_id = lot_final.id
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 2.0,
'mo_id': mo.id,
'lot_id': lot_final.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 5, 'You should have consumed all the 5 product in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 5.0,
'mo_id': mo.id,
'lot_id': lot_final.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_duplicate_move(self):
""" This test creates a MO from 3 different lot on a consumed product (p2).
The unbuild order should revert the correct quantity for each specific lot.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='none', tracking_base_2='lot', tracking_base_1='none')
self.assertEqual(len(mo), 1, 'MO should have been created')
lot_1 = self.env['stock.production.lot'].create({
'name': 'lot_1',
'product_id': p2.id,
})
lot_2 = self.env['stock.production.lot'].create({
'name': 'lot_2',
'product_id': p2.id,
})
lot_3 = self.env['stock.production.lot'].create({
'name': 'lot_3',
'product_id': p2.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_2)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3)
mo.action_assign()
for ml in mo.move_raw_ids.mapped('move_line_ids'):
ml.qty_done = ml.product_qty
produce_wizard = self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}).create({
'product_qty': 5.0,
})
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 0, 'You should have consumed all the 1 product for lot 1 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 3 product for lot 2 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 1, 'You should have consumed only 1 product for lot3 in stock')
self.env['mrp.unbuild'].create({
'product_id': p_final.id,
'bom_id': bom.id,
'product_qty': 5.0,
'mo_id': mo.id,
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have no more final product in stock after unbuild')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3')
@@ -8,7 +8,7 @@
<form string="Configure Manufacturing" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Operations</h2>
+7 -1
View File
@@ -26,14 +26,20 @@
</div>
<field name="is_done" invisible="1"/>
<field name="workorder_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
</group>
</group>
<field name="active_move_line_ids" attrs="{'readonly': [('is_done', '=', True)]}" context="{'default_workorder_id': workorder_id}">
<field name="active_move_line_ids" attrs="{'readonly': [('is_done', '=', True)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree editable="bottom" decoration-success="product_qty==qty_done" decoration-danger="(product_qty &gt; 0) and (qty_done&gt;product_qty)">
<field name="lot_id" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
<field name="product_qty" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
<field name="qty_done"/>
<field name="workorder_id" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="product_uom_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="done_wo" invisible="1"/>
</tree>
</field>
+1 -1
View File
@@ -82,7 +82,7 @@ class MrpProductProduce(models.TransientModel):
quantity = self.product_qty
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_('You should at least produce some quantity'))
for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')):
for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.quantity_done == 0 and x.state not in ('done', 'cancel')):
if move.unit_factor:
rounding = move.product_uom.rounding
move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding)
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_repair
from . import stock_traceability
+1 -9
View File
@@ -12,14 +12,6 @@ class StockMove(models.Model):
_inherit = 'stock.move'
repair_id = fields.Many2one('mrp.repair')
consume_repair_id = fields.Many2one('mrp.repair')
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
repair_id = fields.Many2one('mrp.repair', related='move_id.repair_id')
consume_repair_id = fields.Many2one('mrp.repair', related='move_id.consume_repair_id')
class Repair(models.Model):
@@ -442,7 +434,7 @@ class Repair(models.Model):
'result_package_id': False,
'location_id': operation.location_id.id, #TODO: owner stuff
'location_dest_id': operation.location_dest_id.id,})],
'consume_repair_id': repair.id,
'repair_id': repair.id,
})
moves |= move
operation.write({'move_id': move.id, 'state': 'done'})
@@ -0,0 +1,13 @@
from odoo import models, api
class MrpStockReport(models.TransientModel):
_inherit = 'stock.traceability.report'
@api.model
def get_links(self, move_line):
res_model, res_id, ref = super(MrpStockReport, self).get_links(move_line)
if move_line.move_id.repair_id:
res_model = 'mrp.repair'
res_id = move_line.move_id.repair_id.id
ref = move_line.move_id.repair_id.name
return res_model, res_id, ref
+1 -1
View File
@@ -122,7 +122,7 @@ class PadCommon(models.AbstractModel):
# Update the pad if the `pad_content_field` is modified
for k, field in pycompat.items(self._fields):
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field):
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
company = self.env.user.sudo().company_id
myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
path = self[k].split('/p/')[1]
+1 -1
View File
@@ -55,7 +55,7 @@ var FieldPad = AbstractField.extend({
// Guess what we decided...
var url = {};
url.toJSON = _.constant(this.url);
this._setValue(url);
this._setValue(url, {doNotSetDirty: true});
}
return this._super.apply(this, arguments);
+36
View File
@@ -193,5 +193,41 @@ QUnit.module('pad widget', {
delete FieldPad.prototype.isPadConfigured;
});
QUnit.test('pad widget is not considered dirty at edition', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'task',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<group>' +
'<field name="description" widget="pad"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (!args.method) {
return $.when(true);
}
return this._super.apply(this, arguments);
},
session: {
userName: "batman",
},
});
form.$buttons.find('.o_form_button_edit').click();
var def = form.canBeDiscarded();
assert.strictEqual($('.modal').length, 0,
"should have no confirmation modal opened");
assert.strictEqual(def.state(), 'resolved',
"can be discarded was succesfully resolved");
form.destroy();
delete FieldPad.prototype.isPadConfigured;
});
});
+7 -1
View File
@@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__)
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
INT_CURRENCIES = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV',
'VND', 'XOF'
];
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
@@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model):
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100),
'currency': self.currency_id.name,
'metadata[reference]': self.reference
}
+26 -6
View File
@@ -1,6 +1,14 @@
odoo.define('payment_stripe.stripe', function(require) {
"use strict";
var ajax = require('web.ajax');
// The following currencies are integer only, see
// https://stripe.com/docs/currencies#zero-decimal
var int_currencies = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí',
'RWF', 'KRW', 'VUV', 'VND', 'XOF'
];
var handler = StripeCheckout.configure({
key: $("input[name='stripe_key']").val(),
image: $("input[name='stripe_image']").val(),
@@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) {
e.preventDefault();
if ($('.o_website_payment').length !== 0) {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/website_payment/transaction', 'call', {
reference: $("input[name='invoice_num']").val(),
amount: $("input[name='amount']").val(),
currency_id: $("input[name='currency_id']").val(),
amount: amount,
currency_id: currency,
acquirer_id: acquirer_id
})
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
} else {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
so_id: so_id,
access_token: access_token
@@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) {
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
});
}
+1 -1
View File
@@ -870,7 +870,7 @@ class PosOrderLine(models.Model):
def create(self, values):
if values.get('order_id') and not values.get('name'):
# set name based on the sequence specified on the config
config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id
config_id = self.order_id.browse(values['order_id']).session_id.config_id.id
# HACK: sequence created in the same transaction as the config
# cf TODO master is pos.config create
# remove me saas-15
+1 -1
View File
@@ -23,4 +23,4 @@ class PosInvoiceReport(models.AbstractModel):
not_invoiced_orders_names = [a.name for a in not_invoiced_posorders]
raise UserError(_('No link to an invoice for %s.') % ', '.join(not_invoiced_orders_names))
return self.env['ir.actions.report'].sudo().render_template('account.report_invoice', {'docs': self.env['account.invoice'].sudo().browse(ids_to_print)})
return {'docs': self.env['account.invoice'].sudo().browse(ids_to_print)}
@@ -397,12 +397,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
# I generate an invoice from the order
self.invoice = self.pos_order_pos1.action_pos_order_invoice()
res = self.pos_order_pos1.action_pos_order_invoice()
self.assertIn('res_id', res, "No invoice created")
# I test that the total of the attached invoice is correct
self.amount_total = self.pos_order_pos1.amount_total
invoice = self.env['account.invoice'].browse(res['res_id'])
self.assertEqual(
float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
"""In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""
@@ -25,4 +25,6 @@
<field name="report_name">point_of_sale.report_saledetails</field>
</record>
<template id="report_invoice" inherit_id="account.report_invoice" primary="True"/>
</odoo>
+3 -1
View File
@@ -291,7 +291,9 @@ class ProductProduct(models.Model):
@api.model
def create(self, vals):
product = super(ProductProduct, self.with_context(create_product_product=True)).create(vals)
product._set_standard_price(vals.get('standard_price', 0.0))
# When a unique variant is created from tmpl then the standard price is set by _set_standard_price
if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1):
product._set_standard_price(vals.get('standard_price') or 0.0)
return product
@api.multi
+1 -2
View File
@@ -113,8 +113,7 @@ class Pricelist(models.Model):
uom_id = self._context['uom']
if uom_id:
# rebrowse with uom if given
product_ids = [item[0].id for item in products_qty_partner]
products = self.env['product.product'].with_context(uom=uom_id).browse(product_ids)
products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
else:
products = [item[0] for item in products_qty_partner]
+4 -2
View File
@@ -171,7 +171,9 @@ class ProductTemplate(models.Model):
# Support context pricelists specified as display_name or ID for compatibility
if isinstance(pricelist_id_or_name, basestring):
pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
if pricelist_data:
pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0])
elif isinstance(pricelist_id_or_name, pycompat.integer_types):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
@@ -282,7 +284,7 @@ class ProductTemplate(models.Model):
tools.image_resize_images(vals)
template = super(ProductTemplate, self).create(vals)
if "create_product_product" not in self._context:
template.create_variant_ids()
template.with_context(create_from_tmpl=True).create_variant_ids()
# This is needed to set given values to first variant after creation
related_vals = {}
@@ -8,7 +8,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Task Management</h2>
+3
View File
@@ -668,7 +668,10 @@ class PurchaseOrderLine(models.Model):
diff_qty += move.product_qty
move.do_unreserve()
move.state = 'draft'
picking = move.picking_id
move.unlink()
if not picking.move_lines:
picking.unlink()
else:
raise UserError('You cannot decrease the ordered quantity below the received quantity.\n'
'Create a return first.')
@@ -7,7 +7,7 @@
<form string="Configure Purchases" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="po_double_validation" invisible="1"/>
+3 -3
View File
@@ -69,7 +69,7 @@ class Location(models.Model):
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !')]
@api.one
@api.depends('name', 'location_id')
@api.depends('name', 'location_id.name')
def _compute_complete_name(self):
""" Forms complete name of location from parent location to child location. """
name = self.name
@@ -117,9 +117,9 @@ class Location(models.Model):
('view_location_id.parent_left', '<=', self.parent_left),
('view_location_id.parent_right', '>=', self.parent_left)], limit=1)
def should_impact_quants(self):
def should_bypass_reservation(self):
self.ensure_one()
return False if self.usage in ('supplier', 'inventory', 'production', 'customer') else True
return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location
class Route(models.Model):
+22 -25
View File
@@ -121,6 +121,7 @@ class StockMove(models.Model):
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous,"
"this second option should be chosen.")
scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
scrap_ids = fields.One2many('stock.scrap', 'move_id')
procurement_id = fields.Many2one('procurement.order', 'Procurement')
group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id)
rule_id = fields.Many2one('procurement.rule', 'Procurement Rule', ondelete='restrict', help='The procurement rule that created this stock move')
@@ -430,8 +431,8 @@ class StockMove(models.Model):
'res_id': self.id,
'context': dict(
self.env.context,
show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done'), # able to create lots, whatever the value of ` use_create_lots`.
show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done',
show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`.
show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id,
show_source_location=self.location_id.child_ids,
show_destination_location=self.location_dest_id.child_ids,
show_package=not self.location_id.usage == 'supplier',
@@ -441,14 +442,6 @@ class StockMove(models.Model):
# Misc tools
# ------------------------------------------------------------
def _filter_closed_moves(self):
""" Helper methods when having to avoid working on moves that are
already done or canceled. In a lot of cases you may handle a batch
of stock moves, some being already done / canceled, other being still
under computation. Instead of having to use filtered everywhere and
forgot some of them, use this tool instead. """
return self.filtered(lambda move: move.state not in ('done', 'cancel'))
@api.multi
def do_unreserve(self):
if any(move.state in ('done', 'cancel') for move in self):
@@ -713,18 +706,18 @@ class StockMove(models.Model):
equal to its `product_qty`. If it is less, the stock move is considered
partially available.
"""
assigned_moves = self.env['stock.move']
partially_available_moves = self.env['stock.move']
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
or move.product_id.type == 'consu':
# create the move line(s) but do not impact quants
if move.product_id.tracking == 'serial':
for i in range(0, int(move.product_qty)):
move_line_id = self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
move.write({'move_line_ids': [(4, move_line_id.id, 0)]})
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
else:
move_line_id = self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=move.product_qty))
move.write({'move_line_ids': [(4, move_line_id.id, 0)]})
move.write({'state': 'assigned'})
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=move.product_qty))
assigned_moves |= move
else:
if not move.move_orig_ids:
if move.procure_method == 'make_to_order':
@@ -736,9 +729,9 @@ class StockMove(models.Model):
need = move.product_qty - move.reserved_availability
taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, strict=False)
if need == taken_quantity:
move.state = 'assigned'
assigned_moves |= move
else:
move.state = 'partially_available'
partially_available_moves |= move
else:
# Check what our parents brought and what our siblings took in order to
# determine what we can distribute.
@@ -749,7 +742,6 @@ class StockMove(models.Model):
keys_in = ['location_dest_id', 'lot_id', 'result_package_id', 'owner_id']
grouped_move_lines_in = {}
for k, g in groupby(sorted(move_lines_in, key=itemgetter(*keys_in)), key=itemgetter(*keys_in)):
# `qty_done` is in `ml.product_uom_id` and, as we will later increase the
qty_done = 0
for ml in g:
qty_done += ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
@@ -757,9 +749,12 @@ class StockMove(models.Model):
move_lines_out_done = (move.move_orig_ids.mapped('move_dest_ids') - move)\
.filtered(lambda m: m.state in ['done'])\
.mapped('move_line_ids')
move_lines_out_reserved = (move.move_orig_ids.mapped('move_dest_ids') - move)\
.filtered(lambda m: m.state in ['partially_available', 'assigned'])\
.mapped('move_line_ids')
# As we defer the write on the stock.move's state at the end of the loop, there
# could be moves to consider in what our siblings already took.
moves_out_siblings = move.move_orig_ids.mapped('move_dest_ids') - move
moves_out_siblings_to_consider = moves_out_siblings & (assigned_moves + partially_available_moves)
reserved_moves_out_siblings = moves_out_siblings.filtered(lambda m: m.state in ['partially_available', 'assigned'])
move_lines_out_reserved = (reserved_moves_out_siblings | moves_out_siblings_to_consider).mapped('move_line_ids')
keys_out = ['location_id', 'lot_id', 'package_id', 'owner_id']
grouped_move_lines_out = {}
for k, g in groupby(sorted(move_lines_out_done, key=itemgetter(*keys_out)), key=itemgetter(*keys_out)):
@@ -782,11 +777,12 @@ class StockMove(models.Model):
need = move.product_qty - sum(move.move_line_ids.mapped('product_qty'))
taken_quantity = move._update_reserved_quantity(need, quantity, location_id, lot_id, package_id, owner_id)
if need - taken_quantity == 0.0:
move.state = 'assigned'
assigned_moves |= move
break
if move.state != 'partially_available':
move.state = 'partially_available'
self.mapped('picking_id')._check_entire_pack()
partially_available_moves |= move
partially_available_moves.write({'state': 'partially_available'})
assigned_moves.write({'state': 'assigned'})
self.mapped('picking_id')._check_entire_pack()
@api.multi
def action_cancel(self):
@@ -910,6 +906,7 @@ class StockMove(models.Model):
})
picking.message_post('Backorder Created') #message needs to be improved
moves_to_backorder.write({'picking_id': backorder_picking.id})
moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
moves_to_backorder.action_assign()
return moves_todo
+32 -38
View File
@@ -36,7 +36,7 @@ class StockMoveLine(models.Model):
'stock.quant.package', 'Destination Package',
ondelete='restrict', required=False,
help="If set, the operations are packed into this package")
date = fields.Datetime('Date', default=fields.Datetime.now(), required=True)
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants")
location_id = fields.Many2one('stock.location', 'From', required=True)
location_dest_id = fields.Many2one('stock.location', 'To', required=True)
@@ -96,10 +96,10 @@ class StockMoveLine(models.Model):
vals['ordered_qty'] = vals.get('product_uom_qty')
ml = super(StockMoveLine, self).create(vals)
if ml.state == 'done':
Quant = self.env['stock.quant']
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
in_date = None
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
in_date = None
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if available_qty < 0 and ml.lot_id:
# see if we can compensate the negative quants with some untracked quants
@@ -108,7 +108,6 @@ class StockMoveLine(models.Model):
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
next_moves = ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
next_moves.do_unreserve()
@@ -125,12 +124,13 @@ class StockMoveLine(models.Model):
return super(StockMoveLine, self).write(vals)
Quant = self.env['stock.quant']
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
# We forbid to change the reserved quantity in the interace, but it is needed in the
# case of stock.move's split.
# TODO Move me in the update
if 'product_uom_qty' in vals:
for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned')):
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned') and m.product_id.type == 'product'):
if not ml.location_id.should_bypass_reservation():
qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
@@ -154,8 +154,8 @@ class StockMoveLine(models.Model):
updates[key] = self.env[model].browse(vals[key])
if updates:
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned']):
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'):
if not ml.location_id.should_bypass_reservation():
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
except UserError:
@@ -164,7 +164,7 @@ class StockMoveLine(models.Model):
else:
raise
if updates.get('location_id', ml.location_id).should_impact_quants() and ml.product_id.type == 'product':
if not updates.get('location_id', ml.location_id).should_bypass_reservation():
new_product_qty = 0
try:
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id),
@@ -186,18 +186,14 @@ class StockMoveLine(models.Model):
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `action_assign` on the concerned moves.
next_moves = self.env['stock.move']
if updates or 'qty_done' in vals:
for ml in self.filtered(lambda ml: ml.move_id.state == 'done'):
for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'):
# undo the original move line
in_date = None
if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -ml.qty_done, lot_id=ml.lot_id,
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -ml.qty_done, lot_id=ml.lot_id,
package_id=ml.package_id, owner_id=ml.owner_id)[1]
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
Quant._update_available_quantity(ml.product_id, ml.location_id, ml.qty_done, lot_id=ml.lot_id,
Quant._update_available_quantity(ml.product_id, ml.location_id, ml.qty_done, lot_id=ml.lot_id,
package_id=ml.package_id, owner_id=ml.owner_id, in_date=in_date)
# move what's been actually done
in_date = None
product_id = ml.product_id
location_id = updates.get('location_id', ml.location_id)
location_dest_id = updates.get('location_dest_id', ml.location_dest_id)
@@ -207,8 +203,9 @@ class StockMoveLine(models.Model):
result_package_id = updates.get('result_package_id', ml.result_package_id)
owner_id = updates.get('owner_id', ml.owner_id)
quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
if location_id.should_impact_quants() and ml.product_id.type == 'product':
if not location_id.should_bypass_reservation():
ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
if not float_is_zero(quantity, precision_digits=precision):
available_qty, in_date = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
if available_qty < 0 and lot_id:
# see if we can compensate the negative quants with some untracked quants
@@ -217,8 +214,8 @@ class StockMoveLine(models.Model):
taken_from_untracked_qty = min(untracked_qty, abs(available_qty))
Quant._update_available_quantity(product_id, location_id, -taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
Quant._update_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
if location_dest_id.should_impact_quants() and ml.product_id.type == 'product' and qty_done:
if not location_id.should_bypass_reservation():
ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date)
# Unreserve and reserve following move in order to have the real reserved quantity on move_line.
next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
@@ -233,8 +230,8 @@ class StockMoveLine(models.Model):
for ml in self:
if ml.state in ('done', 'cancel'):
raise UserError(_('You can not delete pack operations of a done picking'))
# Unlinking a pack operation should unreserve.
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and not float_is_zero(ml.product_qty, precision_digits=precision):
# Unlinking a move line should unreserve.
if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation() and not float_is_zero(ml.product_qty, precision_digits=precision):
self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id,
package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
return super(StockMoveLine, self).unlink()
@@ -290,16 +287,16 @@ class StockMoveLine(models.Model):
# Now, we can actually move the quant.
for ml in self - ml_to_delete:
if ml.product_id.type != 'consu':
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
rounding = ml.product_uom_id.rounding
# if this move line is force assigned, unreserve elsewhere if needed
if float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
if not ml.location_id.should_bypass_reservation() and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
extra_qty = ml.qty_done - ml.product_qty
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
# unreserve what's been reserved
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and ml.product_qty:
if not ml.location_id.should_bypass_reservation() and ml.product_id.type == 'product' and ml.product_qty:
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
except UserError:
@@ -307,18 +304,15 @@ class StockMoveLine(models.Model):
# move what's been actually done
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
in_date = None
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if available_qty < 0 and ml.lot_id:
# see if we can compensate the negative quants with some untracked quants
untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
if untracked_qty:
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
if available_qty < 0 and ml.lot_id:
# see if we can compensate the negative quants with some untracked quants
untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
if untracked_qty:
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
""" When editing a done move line or validating one with some forced quantities, it is
-1
View File
@@ -489,7 +489,6 @@ class Picking(models.Model):
if not moves:
raise UserError(_('Nothing to check the availability for.'))
moves.action_assign()
self._check_entire_pack()
return True
@api.multi
+3 -1
View File
@@ -39,7 +39,9 @@ class ProductionLot(models.Model):
@api.one
def _product_qty(self):
self.product_qty = sum(self.quant_ids.mapped('quantity'))
# We only care for the quants in internal or transit locations.
quants = self.quant_ids.filtered(lambda q: q.location_id.usage in ['internal', 'transit'])
self.product_qty = sum(quants.mapped('quantity'))
@api.multi
def action_traceability(self):
+70 -32
View File
@@ -35,6 +35,7 @@ class MrpStockReport(models.TransientModel):
('lot_id', '=', move_line.lot_id.id),
('location_dest_id', '=', move_line.location_id.id),
('id', '!=', move_line.id),
('date', '<', move_line.date),
])
if res:
res |= self.get_move_lines_upstream(res)
@@ -55,6 +56,8 @@ class MrpStockReport(models.TransientModel):
('product_id', '=', move_line.product_id.id),
('lot_id', '=', move_line.lot_id.id),
('location_id', '=', move_line.location_dest_id.id),
('id', '!=', move_line.id),
('date', '>', move_line.date),
])
if res:
res |= self.get_move_lines_downstream(res)
@@ -79,7 +82,7 @@ class MrpStockReport(models.TransientModel):
if stream == "downstream":
move_ids = self.env['stock.move.line'].search([
('lot_id', '=', context.get('active_id')),
('location_id.usage', '=', 'supplier'),
('location_id.usage', '!=', 'internal'),
('state', '=', 'done'),
])
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
@@ -87,13 +90,14 @@ class MrpStockReport(models.TransientModel):
quant_ids = self.env['stock.quant'].search([
('lot_id', '=', context.get('active_id')),
('quantity', '<', 0),
('location_id.usage', '=', 'internal'),
])
res += self._lines(line_id, model_id=model_id, model='stock.quant', level=level,
parent_quant=parent_quant, stream=stream, obj_ids=quant_ids)
else:
move_ids = self.env['stock.move.line'].search([
('lot_id', '=', context.get('active_id')),
('location_dest_id.usage', '=', 'customer'),
('location_dest_id.usage', '!=', 'internal'),
('state', '=', 'done'),
])
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
@@ -101,6 +105,7 @@ class MrpStockReport(models.TransientModel):
quant_ids = self.env['stock.quant'].search([
('lot_id', '=', context.get('active_id')),
('quantity', '>', 0),
('location_id.usage', '=', 'internal'),
])
res += self._lines(line_id, model_id=model_id, model='stock.quant', level=level,
parent_quant=parent_quant, stream=stream, obj_ids=quant_ids)
@@ -112,7 +117,12 @@ class MrpStockReport(models.TransientModel):
res = self._lines(line_id, model_id=context.get('active_id'), model=context.get('model'), level=level, parent_quant=parent_quant, stream=stream, obj_ids=move_line_ids)
else:
res = self._lines(line_id, model_id=model_id, model=model, level=level, parent_quant=parent_quant, stream=stream)
return res
reverse_sort = True
if stream == "downstream":
reverse_sort = False
final_vals = sorted(res, key=lambda v: v['date'], reverse=reverse_sort)
lines = self.final_vals_to_lines(final_vals, level)
return lines
@api.model
def get_links(self, move_line):
@@ -126,14 +136,18 @@ class MrpStockReport(models.TransientModel):
elif move_line.move_id.inventory_id:
res_model = 'stock.inventory'
res_id = move_line.move_id.inventory_id.id
ref = move_line.move_id.inventory_id.name
ref = 'Inv. Adj.: ' + move_line.move_id.inventory_id.name
elif move_line.move_id.scrapped and move_line.move_id.scrap_ids:
res_model = 'stock.scrap'
res_id = move_line.move_id.scrap_ids[0].id
ref = move_line.move_id.scrap_ids[0].name
return res_model, res_id, ref
def make_dict_move(self, level, parent_id, move_line, stream=False):
def make_dict_move(self, level, parent_id, move_line, stream=False, unfoldable=False):
res_model, res_id, ref = self.get_links(move_line)
data = [{
'level': level,
'unfoldable': False,
'unfoldable': unfoldable,
'date': move_line.move_id.date,
'parent_id': parent_id,
'model_id': move_line.id,
@@ -150,6 +164,9 @@ class MrpStockReport(models.TransientModel):
def make_dict_head(self, level, parent_id, model=False, stream=False, move_line=False):
data = []
product_id_name = move_line.product_id.display_name
if move_line.lot_id:
product_id_name += ' ('+move_line.lot_id.name+')'
if model == 'stock.move.line':
data = [{
'level': level,
@@ -158,10 +175,9 @@ class MrpStockReport(models.TransientModel):
'model_id': move_line.id,
'parent_id': parent_id,
'model': model or 'stock.move.line',
'product_id': move_line.product_id.display_name+' ('+move_line.lot_id.name+')',
'product_id': product_id_name,
'product_qty_uom': str(move_line.qty_done) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_id.name,
'location_destination': move_line.location_dest_id.name,
'location_source': move_line.location_dest_id.name,
'stream': stream,
'reference_id': False}]
elif model == 'stock.quant':
@@ -172,7 +188,7 @@ class MrpStockReport(models.TransientModel):
'model_id': move_line.id,
'parent_id': parent_id,
'model': model or 'stock.quant',
'product_id': move_line.product_id.display_name + ' (' + move_line.lot_id.name + ')',
'product_id': product_id_name,
'product_qty_uom': str(move_line.quantity) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_id.name,
'stream': stream,
@@ -180,13 +196,10 @@ class MrpStockReport(models.TransientModel):
return data
@api.model
def upstream_traceability(self, level, stream=False, line_id=False, model=False, model_id=False, parent_quant=False):
model_obj = self.env[model].browse(model_id)
def upstream_traceability(self, level, stream=False, line_id=False, model=False, model_obj=False, parent_quant=False):
final_vals =[]
if model == 'stock.move.line':
moves = self.get_move_lines_upstream(model_obj)
for move in moves.sorted(key=lambda m: m.date, reverse=True):
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move)
elif model == 'stock.quant':
moves = self.env['stock.move.line'].search([
('location_dest_id', '=', model_obj.location_id.id),
@@ -195,18 +208,18 @@ class MrpStockReport(models.TransientModel):
('state', '=', 'done'),
])
moves |= self.get_move_lines_upstream(moves)
for move in moves.sorted(key=lambda m: m.date, reverse=True):
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move)
for move in moves:
unfoldable = False
if move.consume_line_ids:
unfoldable = True
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move, unfoldable=unfoldable)
return final_vals
@api.model
def downstream_traceability(self, level, stream=False, line_id=False, model=False, model_id=False, parent_quant=False):
model_obj = self.env[model].browse(model_id)
def downstream_traceability(self, level, stream=False, line_id=False, model=False, model_obj=False, parent_quant=False):
final_vals = []
if model == 'stock.move.line':
moves = self.get_move_lines_downstream(model_obj)
for move in moves.sorted(key=lambda r: r.date):
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move)
elif model == 'stock.quant':
moves = self.env['stock.move.line'].search([
('location_id', '=', model_obj.location_id.id),
@@ -215,22 +228,16 @@ class MrpStockReport(models.TransientModel):
('state', '=', 'done'),
])
moves |= self.get_move_lines_downstream(moves)
for move in moves.sorted(key=lambda r: r.date):
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move)
for move in moves:
unfoldable = False
if move.produce_line_ids:
unfoldable = True
final_vals += self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=move, unfoldable=unfoldable)
return final_vals
@api.model
def _lines(self, line_id=None, model_id=False, model=False, level=0, parent_quant=False, stream=False, obj_ids=[], **kw):
def final_vals_to_lines(self, final_vals, level):
lines = []
final_vals = []
if model and line_id:
if stream == "downstream":
final_vals += self.downstream_traceability(level, stream='downstream', line_id=line_id, model=model, model_id=model_id, parent_quant=parent_quant)
else:
final_vals += self.upstream_traceability(level, stream='upstream', line_id=line_id, model=model, model_id=model_id, parent_quant=parent_quant)
else:
for move_line in obj_ids:
final_vals += self.make_dict_head(level, stream=stream, parent_id=line_id, model=model or 'stock.move.line', move_line=move_line)
for data in final_vals:
lines.append({
'id': autoIncrement(),
@@ -254,6 +261,37 @@ class MrpStockReport(models.TransientModel):
})
return lines
@api.model
def _lines(self, line_id=None, model_id=False, model=False, level=0, parent_quant=False, stream=False, obj_ids=[], **kw):
final_vals = []
if model and line_id:
model_obj = self.env[model].browse(model_id)
if stream == "downstream":
final_vals += self.downstream_traceability(level, stream='downstream', line_id=line_id, model=model, model_obj=model_obj, parent_quant=parent_quant)
if model == 'stock.move.line':
if model_obj.produce_line_ids:
final_vals += self.get_produced_or_consumed_vals(model_obj.produce_line_ids, level, model=model, stream=stream, parent_id=line_id)
else:
final_vals = self.make_dict_move(level, stream=stream, parent_id=line_id,move_line=model_obj) + final_vals
else:
final_vals += self.upstream_traceability(level, stream='upstream', line_id=line_id, model=model, model_obj=model_obj, parent_quant=parent_quant)
if model == 'stock.move.line':
if model_obj.consume_line_ids:
final_vals += self.get_produced_or_consumed_vals(model_obj.consume_line_ids, level, model=model, stream=stream, parent_id=line_id)
else:
final_vals = self.make_dict_move(level, stream=stream, parent_id=line_id, move_line=model_obj) + final_vals
else:
for move_line in obj_ids:
final_vals += self.make_dict_head(level, stream=stream, parent_id=line_id, model=model or 'stock.pack.operation', move_line=move_line)
return final_vals
@api.model
def get_produced_or_consumed_vals(self, move_lines, level, model, stream, parent_id):
final_vals = []
for line in move_lines:
final_vals += self.make_dict_head(level, model=model, stream=stream, parent_id=parent_id, move_line=line)
return final_vals
@api.multi
def get_pdf_lines(self, line_data=[]):
final_vals = []
@@ -13,7 +13,7 @@
<t t-name="report_mrp_line">
<t t-set="trclass" t-value="'o_stock_reports_default_style'"/>
<t t-if="l.model == 'stock.move.line'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-if="l.model == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level1'"/></t>
<t t-if="l.model == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-set="space_td" t-value="'margin-left: '+ l.level + 'px;'"/>
<t t-set="domainClass" t-value="'o_stock_reports_domain_line_0'"/>
<t t-if="l.unfoldable == false">
@@ -32,6 +32,26 @@
<span class="o_stock_reports_unfoldable o_stock_reports_caret_icon"><i class="fa fa-fw fa-caret-right"></i></span>
</t>
</t>
<t t-if="l.res_model == 'mrp.production' and l.reference == c">
<span t-if="c">
<a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action"><t t-raw="c" /></a>
</span>
</t>
<t t-if="l.res_model == 'mrp.unbuild' and l.reference == c">
<span t-if="c">
<a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action"><t t-raw="c" /></a>
</span>
</t>
<t t-if="l.res_model == 'mrp.repair' and l.reference == c">
<span t-if="c">
<a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action"><t t-raw="c" /></a>
</span>
</t>
<t t-if="l.res_model == 'stock.scrap' and l.reference == c">
<span t-if="c">
<a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action"><t t-raw="c" /></a>
</span>
</t>
<t t-if="l.res_model == 'stock.inventory' and l.reference == c">
<span t-if="c">
<a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action"><t t-raw="c" /></a>
+4 -3
View File
@@ -72,11 +72,12 @@
!python {model: stock.picking, id: pick1}: |
self.action_done()
-
Check the system created 3 quants one with 120 pieces on pallet 1, one with 120 pieces on pallet 2 with lot A and 60 pieces on pallet 3
Check the system created 5 quants one with 120 pieces on pallet 1, one with 120 pieces on pallet 2 with lot A and 60 pieces on pallet 3 in stock
and 1 with -180 pieces and another with -120 piece lot A in supplier
-
!python {model: stock.quant, id: False}: |
quants = self.search([('product_id','=',ref('product1'))])
assert len(quants.ids) == 3, "The number of quants created is not correct"
assert len(quants.ids) == 5, "The number of quants created is not correct"
for quant in quants:
if quant.package_id.name == 'Pallet 1':
assert quant.quantity == 120, "Should have 120 pieces on pallet 1"
@@ -137,7 +138,7 @@
elif rec.package_id.name == 'Pallet 3' and rec.location_id.id == ref('stock_location_stock'):
assert rec.quantity == 50, "Should have 50 pieces in stock on pallet 3"
else:
assert False, "Unrecognized quant"
assert rec.location_id.id != ref('stock_location_stock'), "Unrecognized quant in stock"
-
Check a backorder was created and on that backorder, prepare partial and process backorder
-
+4 -3
View File
@@ -120,7 +120,7 @@
elif rec.package_id.name == 'Palneg 3' and rec.location_id.id == ref('stock_location_stock'):
pallet_3_stock_qty += rec.quantity
else:
assert False, "Unrecognized quant"
assert rec.location_id.id != ref('stock_location_stock'), "Unrecognized quant in stock"
assert pallet_3_stock_qty == 50, "Should have 50 pieces in stock on pallet 3"
-
Create a picking for reconciling the negative quant
@@ -148,8 +148,9 @@
Check the negative quant was reconciled
-
!python {model: stock.quant, id: False}: |
neg_quants = self.search([('product_id','=', ref('product_neg')), ('quantity', '<', 0)])
assert len(neg_quants.ids) == 0, "Negative quants should have been reconciled"
neg_quants = self.search([('product_id','=', ref('product_neg')), ('quantity', '<', 0), ('location_id.id', '!=',
ref('stock_location_suppliers'))])
assert len(neg_quants) == 0, "Negative quants should have been reconciled"
pick = self.env['stock.picking'].browse(ref('delivery_reconcile'))
customer_quant = self.search([
('product_id', '=', ref('product_neg')),
+7 -6
View File
@@ -51,11 +51,11 @@
!python {model: stock.picking, id: pick1_wise}: |
self.do_transfer()
-
Check the system created 2 quants
Check the system created 3 quants
-
!python {model: stock.quant, id: False}: |
records = self.search([('product_id','=',ref('product_wise'))])
assert len(records.ids) == 2, "The number of quants created is not correct"
assert len(records.ids) == 3, "The number of quants created is not correct"
-
Make a delivery order of 5 pieces to the customer
-
@@ -138,7 +138,8 @@
Check there was no negative quant created by this picking
-
!python {model: stock.quant, id: False}: |
records = self.search([('product_id','=',ref('product_wise')), ('quantity', '<', 0.0)])
records = self.search([('product_id','=',ref('product_wise')), ('quantity', '<', 0.0), ('location_id.id', '=',
ref('stock_location_stock'))])
assert len(records.ids) == 0, 'This should not have created a negative quant'
-
Check the other delivery order has changed its state back to partially available
@@ -149,10 +150,10 @@
Process the second picking
-
!python {model: stock.picking, id: delivery_order_wise2}: |
self.do_transfer()
self.action_done()
-
Check all quants are in Customers and there are no negative quants anymore
-
!python {model: stock.quant, id: False}: |
records = self.search([('product_id','=',ref('product_wise'))])
assert all([x.location_id.id==ref('stock_location_customers') and x.qty > 0.0 for x in records]), "Negative quant or wrong location detected"
records = self.search([('product_id','=',ref('product_wise')), ('location_id', '!=', ref('stock_location_suppliers'))])
assert all([x.location_id.id==ref('stock_location_customers') and x.quantity > 0.0 for x in records]), "Negative quant or wrong location detected"
+18 -11
View File
@@ -70,9 +70,9 @@ class StockMove(TransactionCase):
move1.action_done()
self.assertEqual(move1.state, 'done')
# no quants are created in the supplier location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.supplier_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.supplier_location), -100.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 100.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.supplier_location)), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.supplier_location)), 1.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
def test_in_2(self):
@@ -111,9 +111,9 @@ class StockMove(TransactionCase):
self.assertEqual(move1.state, 'done')
# no quants are created in the supplier location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 5.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.supplier_location)), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.supplier_location)), 1.0)
quants = self.env['stock.quant']._gather(self.product3, self.stock_location)
self.assertEqual(len(quants), 1.0)
for quant in quants:
@@ -160,11 +160,11 @@ class StockMove(TransactionCase):
self.assertEqual(move1.product_qty, 5) # don't change reservation
self.assertEqual(move1.state, 'done')
# no quants are created in the supplier location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.supplier_location), 0.0)
# Quant balance should result with 5 quant in supplier and stock
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.supplier_location), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 5.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.supplier_location)), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.supplier_location)), 5.0)
quants = self.env['stock.quant']._gather(self.product2, self.stock_location)
self.assertEqual(len(quants), 5.0)
for quant in quants:
@@ -211,9 +211,9 @@ class StockMove(TransactionCase):
# validation
move1.action_done()
self.assertEqual(move1.state, 'done')
# # no quants are created in the customer location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.customer_location)), 0.0)
# Check there is one quant in customer location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 100.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.customer_location)), 1.0)
# there should be no quant amymore in the stock location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0)
@@ -260,7 +260,7 @@ class StockMove(TransactionCase):
# validation
move1.action_done()
self.assertEqual(move1.state, 'done')
# # no quants are created in the customer location
# no quants are created in the customer location since it's a consumable
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.customer_location)), 0.0)
# there should be no quant amymore in the stock location
@@ -2466,12 +2466,14 @@ class StockMove(TransactionCase):
move2.action_done()
initial_in_date_lot2 = self.env['stock.quant'].search([
('location_id', '=', self.stock_location.id),
('product_id', '=', self.product3.id),
('lot_id', '=', lot2.id),
]).in_date
# Edit lot1's incoming date.
quant_lot1 = self.env['stock.quant'].search([
('location_id', '=', self.stock_location.id),
('product_id', '=', self.product3.id),
('lot_id', '=', lot1.id),
])
@@ -2508,6 +2510,7 @@ class StockMove(TransactionCase):
# Check that lot1 correctly is back to stock with its right in_date
quant_lot1 = self.env['stock.quant'].search([
('location_id.usage', '=', 'internal'),
('product_id', '=', self.product3.id),
('lot_id', '=', lot1.id),
])
@@ -2516,6 +2519,7 @@ class StockMove(TransactionCase):
# Check that lo2 is in pack with is right in_date
quant_lot2 = self.env['stock.quant'].search([
('location_id.usage', '=', 'internal'),
('product_id', '=', self.product3.id),
('lot_id', '=', lot2.id),
])
@@ -2567,12 +2571,14 @@ class StockMove(TransactionCase):
move2.action_done()
initial_in_date_lot2 = self.env['stock.quant'].search([
('location_id', '=', self.stock_location.id),
('product_id', '=', self.product3.id),
('lot_id', '=', lot2.id),
]).in_date
# Edit lot1's incoming date.
quant_lot1 = self.env['stock.quant'].search([
('location_id.usage', '=', 'internal'),
('product_id', '=', self.product3.id),
('lot_id', '=', lot1.id),
])
@@ -2606,6 +2612,7 @@ class StockMove(TransactionCase):
})
quants = self.env['stock.quant'].search([
('location_id.usage', '=', 'internal'),
('product_id', '=', self.product3.id),
])
self.assertEqual(len(quants), 2)
+10 -7
View File
@@ -1415,9 +1415,9 @@ class TestStockFlow(TestStockCommon):
# Check picking_in_package is in picking_pack_package
self.assertEqual(picking_in_package.id, picking_pack_package.id, 'The package created in the picking in is not in the one created in picking pack')
self.assertEqual(picking_pack_package.id, picking_out_package.id, 'The package created in the picking in is not in the one created in picking pack')
# Check that we haven't quant in customer location.
quant = self.StockQuantObj.search([('location_id', '=', self.customer_location)])
self.assertEqual(len(quant), 0, 'There should be no package for customer location')
# Check that we have one quant in customer location.
quant = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.customer_location)])
self.assertEqual(len(quant), 1, 'There should be one quant with package for customer location')
# Check that the parent package of the quant is the picking_in_package
def test_50_create_in_out_with_product_pack_lines(self):
@@ -1471,9 +1471,11 @@ class TestStockFlow(TestStockCommon):
packout2.package_id = pack2
packout2.qty_done = 1
picking_out.action_done()
# Check there are no negative quants
# Should be only 1 negative quant in supplier location
neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('quantity', '<', 0.0)])
self.assertEqual(len(neg_quants), 0, 'There are negative quants!')
self.assertEqual(len(neg_quants), 1, 'There should be 1 negative quants for supplier!')
self.assertEqual(neg_quants.location_id.id, self.supplier_location, 'There shoud be 1 negative quants for supplier!')
quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
self.assertEqual(len(quants), 2, 'We should have exactly 2 quants in the end')
@@ -1538,9 +1540,10 @@ class TestStockFlow(TestStockCommon):
'product_uom_id': self.productE.uom_id.id,
})
picking_out.action_done()
# Check there are no negative quants
# Should be only 1 negative quant in supplier location
neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('quantity', '<', 0.0)])
self.assertEqual(len(neg_quants), 0, 'There are negative quants!')
self.assertEqual(len(neg_quants), 1, 'There should be 1 negative quants for supplier!')
self.assertEqual(neg_quants.location_id.id, self.supplier_location, 'There shoud be 1 negative quants for supplier!')
# We should also make sure that when matching stock moves with pack operations, it takes the correct
quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
self.assertEqual(len(quants), 0, 'We should have no quants in the end')
+5 -5
View File
@@ -37,7 +37,7 @@ class TestWarehouse(TestStockCommon):
self.assertEqual(inventory.move_ids.location_dest_id, self.env.ref('stock.location_inventory')) # Inventory loss
self.assertEqual(inventory.move_ids.state, 'done')
quants = self.env['stock.quant']._gather(self.product_1, self.env.ref('stock.location_inventory'))
self.assertEqual(len(quants), 0) # No quant created for inventory loss
self.assertEqual(len(quants), 1) # One quant created for inventory loss
# Check quantity of product in various locations: current, its parent, brother and other
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.lot_stock_id).quantity, 35.0)
@@ -67,9 +67,9 @@ class TestWarehouse(TestStockCommon):
self.assertEqual(inventory.line_ids.product_id, self.product_1)
self.assertEqual(inventory.line_ids.product_qty, 50.0)
# Check associated quants: 1 quant for the product and the quantity
# Check associated quants: 2 quants for the product and the quantity (1 in stock, 1 in inventory adjustment)
quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
self.assertEqual(len(quant), 1)
self.assertEqual(len(quant), 2)
# print quant.name, quant.product_id, quant.location_id
# TDE TODO: expand this test
@@ -225,9 +225,9 @@ class TestWarehouse(TestStockCommon):
quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(len(quants), 0)
# There should be no quant in the inventory loss location
# There should be one quant in the inventory loss location
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
self.assertEqual(len(quant), 0)
self.assertEqual(len(quant), 1)
class TestResupply(TestStockCommon):
@@ -5,7 +5,7 @@
<t t-foreach="lines" t-as="a">
<t t-set="trclass" t-value="'o_stock_reports_default_style'"/>
<t t-if="a.get('model') == 'stock.move.line'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-if="a.get('model') == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level1'"/></t>
<t t-if="a.get('model') == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-set="domainClass" t-value="'o_stock_reports_domain_line'"/>
<t t-if="a['unfoldable'] == false">
<t t-set="domainClass" t-value="'o_stock_reports_domain_line_1'"/>
@@ -30,10 +30,6 @@
</td>
</t>
<td>
<t t-if="a.get('model') == 'stock.move.line'">
<span t-if="a['stream'] == 'downstream'" class="o_stock_reports_stream text-muted" data-stream="upstream" title="Upstream Traceability"><i class="fa fa-arrow-up"></i></span>
<span t-if="a['stream'] == 'upstream'" class="o_stock_reports_stream" data-stream="downstream" title="Downstream Traceability"><i class="fa fa-arrow-down"></i></span>
</t>
</td>
</tr>
</t>
@@ -102,7 +98,7 @@
<t t-set="space_td" t-value="'margin-left: '+ str(a['level']) + 'px;'"/>
<t t-set="trclass" t-value="'o_stock_reports_default_style'"/>
<t t-if="a.get('model') == 'stock.move.line'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-if="a.get('model') == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level1'"/></t>
<t t-if="a.get('model') == 'stock.quant'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
<t t-set="column" t-value="0" />
<tr t-att-data-type="a['type']" t-att-data-stream="a['stream']" t-att-data-id="a['id']" t-att-data-model="a['model']" t-att-class="trclass">
<t t-set="tdclass" t-value="''" />
@@ -8,7 +8,7 @@
<form string="Configure Warehouse" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
+2 -2
View File
@@ -213,7 +213,7 @@
<button name="action_cancel" states="assigned,confirmed,partially_available,draft,waiting" string="Cancel" groups="base.group_user" type="object"/>
<field name="picking_type_code" invisible="1"/>
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available'))]}"/>
<field name="show_operations" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="move_line_exist" invisible="1"/>
<field name="has_packages" invisible="1"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', '&amp;', ('picking_type_code', '!=', 'incoming'), ('state', 'in', ('draft', 'cancel', 'waiting')), '&amp;', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done')]}"/>
@@ -288,7 +288,7 @@
<field name="picking_code" invisible="1"/>
<field name="product_type" invisible="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="show_operations" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
<field name="product_id" required="1" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
+2 -2
View File
@@ -48,7 +48,7 @@
<field name="owner_id" groups="stock.group_tracking_owner"/>
</group>
<group>
<field name="in_date"/>
<field name="in_date" attrs="{'invisible': [('lot_id', '=', False)]}" groups="stock.group_production_lot"/>
</group>
</group>
</form>
@@ -68,7 +68,6 @@
<field name="owner_id" groups="stock.group_tracking_owner"/>
<field name="lot_id" groups="stock.group_production_lot"/>
<field name="package_id" groups="stock.group_tracking_lot"/>
<field name="in_date"/>
<field name='company_id' groups="base.group_multi_company"/>
</tree>
</field>
@@ -91,6 +90,7 @@
<field name="res_model">stock.quant</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,pivot</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_transit_loc': 1}</field>
<field name="help">This analysis gives you a fast overview on the current stock level of your products and their current inventory value.</field>
</record>
<record model="ir.actions.act_window" id="product_template_open_quants"> <!-- Not used -->
+6 -6
View File
@@ -685,7 +685,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception as e:
error = "Database creation error: %s" % str(e) or repr(e)
error = "Database creation error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -696,7 +696,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database duplication error: %s" % str(e) or repr(e)
error = "Database duplication error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -706,7 +706,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database deletion error: %s" % str(e) or repr(e)
error = "Database deletion error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -724,7 +724,7 @@ class Database(http.Controller):
return response
except Exception as e:
_logger.exception('Database.backup')
error = "Database backup error: %s" % str(e) or repr(e)
error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -734,7 +734,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database restore error: %s" % str(e) or repr(e)
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -743,7 +743,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Master password update error: %s" % str(e) or repr(e)
error = "Master password update error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
+18 -15
View File
@@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
custom_events: {
execute_action: function(event) {
var data = event.data;
this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed)
this.do_execute_action(data.action_data, data.env, data.on_closed)
.then(data.on_success, data.on_fail);
},
search: function(event) {
@@ -554,16 +554,19 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
* @param {string} [action_data.effect] if given, a visual effect (a
* rainbowman by default) will be displayed when the action is complete,
* with the string (evaluated) given as options.
* @param {DataSet} dataset a dataset object used to communicate with the server
* @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set)
* @param {Object} env
* @param {string} env.model the model of the record(s) triggering the action
* @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
* @param {integer} [env.currentID] the id of the record triggering the action
* @param {Object} [env.context] a context to pass to the action
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
do_execute_action: function (action_data, model, res_ids, on_closed) {
do_execute_action: function (action_data, env, on_closed) {
var self = this;
var result_handler = on_closed || function () {};
var context = new Context(this.env.context, action_data.context || {});
var context = new Context(env.context, action_data.context || {});
// OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
var record_id = res_ids && res_ids[0] || null;
var recordID = env.currentID || null;
// response handler
var handler = function (action) {
@@ -585,11 +588,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}))
);
ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({active_model: env.model});
if (recordID) {
ncontext.add({
active_id: record_id,
active_ids: res_ids,
active_id: recordID,
active_ids: [recordID],
});
}
ncontext.add(action.context || {});
@@ -611,7 +614,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (action_data.special) {
return handler({"type":"ir.actions.act_window_close"});
} else if (action_data.type === "object") {
var args = res_ids ? [res_ids] : [this.env.ids];
var args = recordID ? [[recordID]] : [env.resIDs];
if (action_data.args) {
try {
// Warning: quotes and double quotes problem due to json and xml clash
@@ -623,13 +626,13 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
}
args.push(context);
var dataset = new data.DataSet(this, model, this.env.context);
var dataset = new data.DataSet(this, env.model, env.context);
return dataset.call_button(action_data.name, args).then(handler);
} else if (action_data.type === "action") {
return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
active_model: this.env.modelName,
active_ids: this.env.ids,
active_id: record_id,
active_model: env.model,
active_ids: env.resIDs,
active_id: recordID,
})).then(handler);
}
},
@@ -219,10 +219,16 @@ var FieldMany2One = AbstractField.extend({
/**
* @private
* @param {string} name
* @returns {Deferred} resolved after the name_create or when the slowcreate
* modal is closed.
*/
_quickCreate: function (name) {
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
var def = $.Deferred();
var slowCreate = function () {
var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
dialog.on('closed', self, def.resolve.bind(def));
};
if (this.nodeOptions.quick_create) {
this.trigger_up('mutexify', {
action: function () {
@@ -235,12 +241,16 @@ var FieldMany2One = AbstractField.extend({
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
}, slowCreate);
def.resolve();
}).fail(function () {
slowCreate();
});
},
});
} else {
slowCreate();
}
return def;
},
/**
* @private
@@ -29,6 +29,7 @@ var ajax = require('web.ajax');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
var Context = require('web.Context');
var AbstractView = Class.extend({
// name displayed in view switchers
@@ -152,9 +153,9 @@ var AbstractView = Class.extend({
});
},
/**
* Returns the view model or create an instance of it if there is noney
* Returns the view model or create an instance of it if none
*
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @return {Object} instance of the view model
*/
getModel: function (parent) {
@@ -167,7 +168,7 @@ var AbstractView = Class.extend({
/**
* Returns the a new view renderer instance
*
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @param {Object} state the information related to the rendered view
* @return {Object} instance of the view renderer
*/
@@ -239,7 +240,7 @@ var AbstractView = Class.extend({
* Loads the subviews for x2many fields when they are not inline
*
* @private
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @returns {Deferred}
*/
_loadSubviews: function (parent) {
@@ -232,13 +232,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return self.isDestroyed() ? $.when() : self.reload();
};
record = record || this.model.get(this.handle);
var recordID = record.data.id;
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
context: record.getContext({additionalContext: attrs.context || {}}),
context: record.getContext({additionalContext: attrs.context || {}}),
}),
model: record.model,
res_ids: [recordID],
env: {
context: record.getContext(),
currentID: record.data.id,
model: record.model,
resIDs: record.res_ids,
},
on_closed: function (reason) {
if (!_.isObject(reason)) {
reload(reason);
@@ -649,7 +649,7 @@ var BasicModel = AbstractModel.extend({
var element = this.localData[id];
if (element.type === 'record') {
if ('currentId' in options && !options.currentId) {
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
@@ -2310,12 +2310,20 @@ var BasicModel = AbstractModel.extend({
});
list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
var relRecordCreated = _.filter(relRecordAdded, function (rec) {
return typeof rec.res_id === 'string';
});
var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id'));
// deliberately generate a single 'replace' command instead
// of a 'delete' and a 'link' commands with the exact diff
// because 1) performance-wise it doesn't change anything
// and 2) to guard against concurrent updates (policy: force
// a complete override of the actual value of the m2m)
commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids));
commands[fieldName].push(x2ManyCommands.replace_with(realIDs));
_.each(relRecordCreated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
});
// generate update commands for records that have been
// updated (it may happen with editable lists)
_.each(relRecordUpdated, function (relRecord) {
@@ -2435,6 +2443,12 @@ var BasicModel = AbstractModel.extend({
*/
_getDomain: function (element, options) {
if (options && options.fieldName) {
if (element._domains[options.fieldName]) {
return Domain.prototype.stringToArray(
element._domains[options.fieldName],
this._getEvalContext(element, true)
);
}
var viewType = options.viewType || element.viewType;
var fieldInfo = element.fieldsInfo[viewType][options.fieldName];
if (fieldInfo && fieldInfo.domain) {
@@ -2697,6 +2711,7 @@ var BasicModel = AbstractModel.extend({
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2870,6 +2885,9 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
if (r._changes[name] === null) {
continue;
}
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
@@ -3021,12 +3039,7 @@ var BasicModel = AbstractModel.extend({
record._warning = true;
}
if (result.domain) {
var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
for (var fieldName in result.domain) {
if (fieldsInfo[fieldName]) {
fieldsInfo[fieldName].domain = result.domain[fieldName];
}
}
record._domains = _.extend(record._domains, result.domain);
}
return self._applyOnChange(result.value, record).then(function () {
return result;
@@ -3083,7 +3096,9 @@ var BasicModel = AbstractModel.extend({
*/
_readGroup: function (list) {
var self = this;
var fields = _.uniq(list.getFieldNames().concat(list.groupedBy));
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var fields = _.uniq(list.getFieldNames().concat(rawGroupBy));
return this._rpc({
model: list.model,
method: 'read_group',
@@ -3094,8 +3109,6 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
@@ -322,10 +322,11 @@ return AbstractRenderer.extend({
element.addClass($render.attr('class'));
var display_hour = '';
if (!event.allDay) {
display_hour = (event.start.format('HH:mm') === '00:00' ? event.r_start.format('HH:mm') : event.start.format('HH:mm')) + ' - ' +
(event.end && event.end.format('HH:mm') !== '00:00' ? event.end.format('HH:mm') : event.r_end.format('HH:mm'));
var start = event.r_start || event.start;
var end = event.r_end || event.end;
display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm');
if (display_hour === '00:00 - 00:00') {
display_hour = _t('All the day');
display_hour = _t('All day');
}
}
element.find('.fc-content .fc-time').text(display_hour);
@@ -217,7 +217,7 @@ var FormController = BasicController.extend({
*
* @private
* @override method from field manager mixin
* @param {string} id
* @param {string} id - id of the previously changed record
* @returns {Deferred}
*/
_confirmSave: function (id) {
@@ -228,15 +228,21 @@ var FormController = BasicController.extend({
return this._setMode('readonly');
}
} else {
// a subrecord changed, so update the corresponding relational field
// A subrecord has changed, so update the corresponding relational field
// i.e. the one whose value is a record with the given id or a list
// having a record with the given id in its data
var record = this.model.get(this.handle);
var fieldsChanged = _.findKey(record.data, function (d) {
return _.isObject(d) &&
(d.id === id || _.findWhere(d.data, {id: id}));
});
return this.renderer.confirmChange(record, record.id, [fieldsChanged]);
// Callback function which returns true
// if a value recursively contains a record with the given id.
// This will be used to determine the list of fields to reload.
var containsChangedRecord = function (value) {
return _.isObject(value) &&
(value.id === id || _.find(value.data, containsChangedRecord));
};
var changedFields = _.findKey(record.data, containsChangedRecord);
return this.renderer.confirmChange(record, record.id, [changedFields]);
}
},
/**
@@ -237,8 +237,12 @@ var KanbanController = BasicController.extend({
}
this.trigger_up('execute_action', {
action_data: attrs,
model: record.model,
res_ids: [record.res_id],
env: {
context: record.getContext(),
currentID: record.res_id,
model: record.model,
resIDs: record.res_ids,
},
on_closed: function () {
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
@@ -240,7 +240,7 @@ var KanbanModel = BasicModel.extend({
*/
_readTooltipFields: function (list) {
var self = this;
var groupedByField = list.fields[list.groupedBy[0]];
var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
return $.when();
}
@@ -282,15 +282,19 @@ var ListRenderer = BasicRenderer.extend({
this._registerModifiers(node, record, $button);
// TODO this should be moved to a handler
var self = this;
$button.on("click", function (e) {
e.stopPropagation();
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: record,
if (record.res_id) {
// TODO this should be moved to a handler
var self = this;
$button.on("click", function (e) {
e.stopPropagation();
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: record,
});
});
});
} else {
$button.prop('disabled', true);
}
return $button;
},
@@ -922,6 +922,140 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('list in form: create with one2many with many2one', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'default_get') {
return $.when({p: [[0, 0, {display_name: 'new record'}]]});
} else if (args.method === 'name_get') {
// This should not be called at all and thus is not accounted for
// in the assert.expect. If this is called, you broke this test.
assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
"should not call name_get for the m2o inside o2m which has no value");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
"should have created the new record in the o2m with the correct name");
form.destroy();
});
QUnit.test('list in form: default_get with x2many create', function (assert) {
assert.expect(3);
this.data.partner.onchanges.timmy = function (obj) {
assert.deepEqual(
obj.timmy,
[
[6, false, []],
[0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}]
],
"should have properly created the x2many command list");
obj.int_field = obj.timmy.length;
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="timmy">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'<field name="int_field"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'default_get') {
return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]});
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy',
"should have created the new record in the m2m with the correct name");
assert.strictEqual($('input.o_field_integer').val(), '2',
"should have called and executed the onchange properly");
form.destroy();
});
QUnit.test('list in form: call button in sub view', function (assert) {
assert.expect(6);
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/get_formview_id') {
return $.when(false);
}
return this._super.apply(this, arguments);
},
intercepts: {
execute_action: function (event) {
assert.strictEqual(event.data.env.model, 'product',
'should call with correct model in env');
assert.strictEqual(event.data.env.currentID, 37,
'should call with correct currentID in env');
assert.deepEqual(event.data.env.resIDs, [37],
'should call with correct resIDs in env');
},
},
archs: {
'product,false,form': '<form string="Partners">' +
'<header>' +
'<button name="action" type="action" string="Just do it !"/>' +
'<button name="object" type="object" string="Just don\'t do it !"/>' +
'<field name="display_name"/>' +
'</header>' +
'</form>',
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('td.o_data_cell:first').click(); // edit first one2many line
form.$('.o_external_button').click(); // open product sub view in modal
$('button:contains("Just do it !")').click(); // click on action button
$('button:contains("Just don\'t do it !")').click(); // click on object button
form.destroy();
});
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -1418,6 +1552,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
trululu: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="p">' +
'<tree editable="bttom">' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return $.when({
domain: {
trululu: domain,
},
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// add a first row with a specific domain for the m2o
domain = [['id', 'in', [10]]]; // domain for subrecord 1
form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
// add a second row with another domain for the m2o
domain = [['id', 'in', [5]]]; // domain for subrecord 2
form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
// check again the first row to ensure that the domain hasn't change
domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
form.$('.o_data_row:first .o_data_cell').click();
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
form.destroy();
});
QUnit.test('updating a many2one from a many2many', function (assert) {
assert.expect(4);
this.data.turtle.records[1].turtle_trululu = 1;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="turtle_trululu"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</form>',
res_id: 1,
archs: {
'partner,false,form': '<form string="Trululu"><field name="display_name"/></form>',
},
mockRPC: function (route, args) {
if (args.method === 'get_formview_id') {
assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
return $.when(false);
}
return this._super(route, args);
},
});
// Opening the modal
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_data_row td:contains(first record)').click();
form.$('.o_external_button').click();
assert.strictEqual($('.modal').length, 1,
"should have one modal in body");
// Changing the 'trululu' value
$('.modal input[name="display_name"]').val('test').trigger('input');
$('.modal button.btn-primary').click();
// Test whether the value has changed
assert.strictEqual($('.modal').length, 0,
"the modal should be closed");
assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test',
"the partner name should have been updated to 'test'");
form.destroy();
});
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -3847,9 +4090,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [2],
assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
assert.strictEqual(event.data.model, 'partner',
assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -3891,9 +4134,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [2],
assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
assert.strictEqual(event.data.model, 'partner',
assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -956,6 +956,30 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('group on date field with magic grouping method', function (assert) {
assert.expect(1);
this.params.fieldNames = ['foo'];
this.params.groupedBy = ['date:month'];
this.params.res_id = undefined;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.deepEqual(args.kwargs.fields, ['foo', 'date'],
"should have correctly trimmed the magic grouping info from the field name");
}
return this._super.apply(this, arguments);
},
});
model.load(this.params);
model.destroy();
});
QUnit.test('read group when grouped by a selection field', function (assert) {
assert.expect(5);
@@ -1949,4 +1973,39 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('reload a new record', function (assert) {
assert.expect(6);
this.params.context = {};
this.params.fieldNames = ['product_id', 'category', 'product_ids'];
this.params.res_id = undefined;
this.params.type = 'record';
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super(route, args);
},
});
model.load(this.params).then(function (recordID) {
model.reload(recordID).then(function (recordID) {
assert.verifySteps(['default_get', 'default_get'],
"two default_get RPCs should have been done");
var record = model.get(recordID);
assert.strictEqual(record.data.product_id, false,
"m2o default value should be false");
assert.deepEqual(record.data.product_ids.data, [],
"o2m default should be []");
assert.deepEqual(record.data.category.data, [],
"m2m default should be []");
});
});
model.destroy();
});
});});
@@ -789,7 +789,7 @@ QUnit.module('Views', {
assert.strictEqual($event1Avatars.find('span').length, 0,
"should not have a span for more attendees since there is only one");
var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars');
assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
@@ -1034,6 +1034,46 @@ QUnit.module('Views', {
"should display 5 events");
calendar.destroy();
});
QUnit.test('events starting at midnight', function (assert) {
assert.expect(2);
var calendar = createView({
View: CalendarView,
model: 'event',
data: this.data,
arch: '<calendar mode="week" date_start="start"/>',
viewOptions: {
initialDate: initialDate,
},
});
// Click on Tuesday 12am
var $view = $('#qunit-fixture').contents();
$view.prependTo('body');
var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5;
var left = calendar.$('.fc-day:eq(2)').offset().left + 5;
try {
testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
} catch (e) {
calendar.destroy();
$view.remove();
throw new Error('The test failed to simulate a click on the screen.' +
'Your screen is probably too small or your dev tools are open.');
}
assert.ok($('.modal-dialog.modal-sm').length,
"should open the quick create dialog");
// Creating the event
$('.modal-body input:first').val('new event in quick create').trigger('input');
$('.modal button.btn:contains(Create)').trigger('click').trigger('click');
assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1,
"should display the new record");
calendar.destroy();
$view.remove();
});
});
});
+8 -7
View File
@@ -903,7 +903,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.strictEqual(event.data.action_data.name, "post",
"should trigger execute_action with correct method name");
assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data");
assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data");
event.data.on_success();
event.data.on_closed();
});
@@ -956,7 +956,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.res_ids, [resID],
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -1006,7 +1006,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.res_ids, [resID],
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -3597,8 +3597,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {test: 2},
"button context should have been evaluated and given to the action, without previous context");
assert.deepEqual(e.data.action_data.context, {
'test': 2
}, "button context should have been evaluated and given to the action, with magicc without previous context");
},
},
});
@@ -3631,8 +3632,8 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {},
"button context should have been evaluated and given to the action, without previous context");
assert.deepEqual(e.data.action_data.context, {
}, "button context should have been evaluated and given to the action, with magic keys but without previous context");
},
},
});
+11 -3
View File
@@ -984,7 +984,7 @@ QUnit.module('Views', {
});
QUnit.test('list view, editable, without data', function (assert) {
assert.expect(9);
assert.expect(11);
this.data.foo.records = [];
@@ -998,6 +998,7 @@ QUnit.module('Views', {
'<field name="date"/>' +
'<field name="m2o"/>' +
'<field name="foo"/>' +
'<button type="object" icon="fa-plus-square" name="method"/>' +
'</tree>',
viewOptions: {
action: {
@@ -1033,7 +1034,14 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), true,
"buttons should be disabled while the record is not yet created");
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), false,
"buttons should not be disabled once the record is created");
list.destroy();
});
@@ -1163,9 +1171,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [1],
assert.deepEqual(event.data.env.currentID, 1,
'should call with correct id');
assert.strictEqual(event.data.model, 'foo',
assert.strictEqual(event.data.env.model, 'foo',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'button_action',
"should call correct method");
+1 -1
View File
@@ -383,7 +383,7 @@ var FieldTextHtml = AbstractField.extend({
* @override
*/
commitChanges: function () {
if (!this.loaded) {
if (!this.loaded || this.mode === 'readonly') {
return;
}
// switch to WYSIWYG mode if currently in code mode to get all changes
@@ -188,4 +188,34 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode
form.destroy();
});
QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
// The 'Save' action may be triggered even in readonly (e.g. when clicking
// on a button in the form view)
assert.expect(0);
var form = testUtils.createView({
View: FormView,
model: 'mass.mailing',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route) {
if (_.str.startsWith(route, '/test')) {
// manually call the callback to simulate that the iframe has
// been correctly loaded
window.odoo[$.deparam(route).callback + '_content'].call();
return $.when();
}
return this._super.apply(this, arguments);
},
});
form.saveRecord(); // before the fix done in this commit, it crashed here
form.destroy();
});
});
@@ -416,8 +416,9 @@ animation.registry.media_video = animation.Class.extend({
if (!this.$target.has('> iframe').length) {
var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
this.$target.html(editor+size);
}
this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
return this._super.apply(this, arguments);
},
});
@@ -9,7 +9,7 @@
<form string="Configure Website" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div>
+2 -1
View File
@@ -170,6 +170,7 @@ class WebsiteEventController(http.Controller):
'event': event,
'main_object': event,
'range': range,
'registrable': event._is_event_registrable()
}
return request.render("website_event.event_description_full", values)
@@ -226,7 +227,7 @@ class WebsiteEventController(http.Controller):
def registration_new(self, event, **post):
tickets = self._process_tickets_details(post)
if not tickets:
return request.redirect("/event/%s" % slug(event))
return False
return request.env['ir.ui.view'].render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event})
def _process_registration_details(self, details):
@@ -311,7 +311,8 @@
<template id="event_description_full">
<t t-call="website_event.event_details">
<div class="col-md-8">
<t t-call="website_event.registration_template"/>
<div t-if="event.state == 'done' or not registrable" class="alert alert-info">Registration for this event is now closed</div>
<t t-if="event.state != 'done' and registrable" t-call="website_event.registration_template"/>
<div class="clearfix"/>
<hr/>
<div itemprop="description" t-field="event.description"></div>
@@ -12,12 +12,7 @@ class WebsiteEventSaleController(WebsiteEventController):
@http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
def event_register(self, event, **post):
event = event.with_context(pricelist=request.website.get_current_pricelist().id)
values = {
'event': event,
'main_object': event,
'range': range,
}
return request.render("website_event.event_description_full", values)
return super(WebsiteEventSaleController, self).event_register(event, **post)
def _process_tickets_details(self, data):
ticket_post = {}
+2 -1
View File
@@ -355,7 +355,8 @@ class WebsiteSale(http.Controller):
values['suggested_products'] = _order._cart_accessories()
if post.get('type') == 'popover':
return request.render("website_sale.cart_popover", values)
# force no-cache so IE11 doesn't cache this XHR
return request.render("website_sale.cart_popover", values, headers={'Cache-Control': 'no-cache'})
return request.render("website_sale.cart", values)
+5 -5
View File
@@ -1160,11 +1160,11 @@
<div class="clearfix" />
<t t-if="mode == ('new', 'billing')">
<div t-attf-class="form-group #{error.get('company_name') and 'has-error' or ''} col-md-6">
<label class="control-label font-weight-normal" for="company_name">Company Name</label>
<label class="control-label font-weight-normal label-optional" for="company_name">Company Name</label>
<input type="text" name="company_name" class="form-control" t-att-value="'company_name' in checkout and checkout['company_name']" />
</div>
<div t-attf-class="form-group #{error.get('vat') and 'has-error' or ''} col-md-6 div_vat">
<label class="control-label font-weight-normal" for="vat">TIN / VAT </label>
<label class="control-label font-weight-normal label-optional" for="vat">TIN / VAT </label>
<input type="text" name="vat" class="form-control" t-att-value="'vat' in checkout and checkout['vat']" />
</div>
</t>
@@ -1174,14 +1174,14 @@
<input type="text" name="street" class="form-control" t-att-value="'street' in checkout and checkout['street']" />
</div>
<div t-attf-class="form-group #{error.get('street2') and 'has-error' or ''} col-md-12 div_street2">
<label class="control-label" for="street2">Street 2</label>
<label class="control-label label-optional" for="street2">Street 2</label>
<input type="text" name="street2" class="form-control" t-att-value="'street2' in checkout and checkout['street2']" />
</div>
<div class="clearfix" />
<t t-set='zip_city' t-value='country and [x for x in country.get_address_fields() if x in ["zip", "city"]] or ["city", "zip"]'/>
<t t-if="'zip' in zip_city and zip_city.index('zip') &lt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
@@ -1191,7 +1191,7 @@
</div>
<t t-if="'zip' in zip_city and zip_city.index('zip') &gt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
+14
View File
@@ -0,0 +1,14 @@
Finland, 2017-08-01
Vizucom Oy agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
We declare that we are authorized and able to make this agreement and sign this declaration.
Signed,
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
List of contributors:
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
Henri Alasentie henri.alasentie@vizucom.com https://github.com/halasentie
+1 -1
View File
@@ -507,7 +507,7 @@ class Module(models.Model):
self._cr.commit()
env = api.Environment(self._cr, self._uid, self._context)
# pylint: disable=next-method-called
config = self.next() or {}
config = env['ir.module.module'].next() or {}
if config.get('type') not in ('ir.actions.act_window_close',):
return config

Some files were not shown because too many files have changed in this diff Show More