[MERGE] forward port branch saas-17 up to 48b2ce60ed
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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&field=image_small&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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 '')"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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 > 0) and (qty_done>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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_repair
|
||||
from . import stock_traceability
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
-
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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': ['|', '&', ('picking_type_code', '!=', 'incoming'), ('state', 'in', ('draft', 'cancel', 'waiting')), '&', ('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': ['&', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"> </div>';
|
||||
var size = '<div class="media_iframe_video_size"> </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>
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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') < 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') > 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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user