[MERGE] forward port branch saas-12 up to 6f8e40c

This commit is contained in:
Christophe Simonis
2016-10-27 18:17:59 +02:00
46 changed files with 671 additions and 209 deletions
+9 -3
View File
@@ -745,7 +745,12 @@ class AccountTax(models.Model):
if not round_tax: if not round_tax:
prec += 5 prec += 5
total_excluded = total_included = base = round(price_unit * quantity, prec)
base_values = self.env.context.get('base_values')
if not base_values:
total_excluded = total_included = base = round(price_unit * quantity, prec)
else:
total_excluded, total_included, base = base_values
# Sorting key is mandatory in this case. When no key is provided, sorted() will perform a # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
# search. However, the search method is overridden in account.tax in order to add a domain # search. However, the search method is overridden in account.tax in order to add a domain
@@ -753,9 +758,10 @@ class AccountTax(models.Model):
# case of group taxes. # case of group taxes.
for tax in self.sorted(key=lambda r: r.sequence): for tax in self.sorted(key=lambda r: r.sequence):
if tax.amount_type == 'group': if tax.amount_type == 'group':
ret = tax.children_tax_ids.compute_all(price_unit, currency, quantity, product, partner) children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base))
ret = children.compute_all(price_unit, currency, quantity, product, partner)
total_excluded = ret['total_excluded'] total_excluded = ret['total_excluded']
base = ret['base'] base = ret['base'] if tax.include_base_amount else base
total_included = ret['total_included'] total_included = ret['total_included']
tax_amount = total_included - total_excluded tax_amount = total_included - total_excluded
taxes += ret['taxes'] taxes += ret['taxes']
@@ -472,7 +472,7 @@ class AccountBankStatementLine(models.Model):
'details': { 'details': {
'name': _("Automatically reconciled items"), 'name': _("Automatically reconciled items"),
'model': 'account.move', 'model': 'account.move',
'ids': automatic_reconciliation_entries.ids 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids
} }
}] }]
return { return {
+19 -17
View File
@@ -612,6 +612,14 @@ class AccountInvoice(models.Model):
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id), 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
} }
# If the taxes generate moves on the same financial account as the invoice line,
# propagate the analytic account from the invoice line to the tax line.
# This is necessary in situations were (part of) the taxes cannot be reclaimed,
# to ensure the tax move is allocated to the proper analytic account.
if not vals.get('account_analytic_id') and line.account_analytic_id and vals['account_id'] == line.account_id.id:
vals['account_analytic_id'] = line.account_analytic_id.id
return vals return vals
@api.multi @api.multi
@@ -622,14 +630,6 @@ class AccountInvoice(models.Model):
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes'] taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
for tax in taxes: for tax in taxes:
val = self._prepare_tax_line_vals(line, tax) val = self._prepare_tax_line_vals(line, tax)
# If the taxes generate moves on the same financial account as the invoice line,
# propagate the analytic account from the invoice line to the tax line.
# This is necessary in situations were (part of) the taxes cannot be reclaimed,
# to ensure the tax move is allocated to the proper analytic account.
if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id:
val['account_analytic_id'] = line.account_analytic_id.id
key = self.env['account.tax'].browse(tax['id']).get_grouping_key(val) key = self.env['account.tax'].browse(tax['id']).get_grouping_key(val)
if key not in tax_grouped: if key not in tax_grouped:
@@ -1350,15 +1350,17 @@ class AccountInvoiceTax(models.Model):
for invoice in self.mapped('invoice_id'): for invoice in self.mapped('invoice_id'):
tax_grouped[invoice.id] = invoice.get_taxes_values() tax_grouped[invoice.id] = invoice.get_taxes_values()
for tax in self: for tax in self:
key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({ tax.base = 0.0
'tax_id': tax.tax_id.id, if tax.tax_id:
'account_id': tax.account_id.id, key = tax.tax_id.get_grouping_key({
'account_analytic_id': tax.account_analytic_id.id, 'tax_id': tax.tax_id.id,
}) 'account_id': tax.account_id.id,
if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]: 'account_analytic_id': tax.account_analytic_id.id,
tax.base = tax_grouped[tax.invoice_id.id][key]['base'] })
else: if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]:
_logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name) tax.base = tax_grouped[tax.invoice_id.id][key]['base']
else:
_logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name)
invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True) invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True)
name = fields.Char(string='Tax Description', required=True) name = fields.Char(string='Tax Description', required=True)
+3
View File
@@ -981,6 +981,9 @@ class AccountMoveLine(models.Model):
return True return True
rec_move_ids = self.env['account.partial.reconcile'] rec_move_ids = self.env['account.partial.reconcile']
for account_move_line in self: for account_move_line in self:
for invoice in account_move_line.payment_id.invoice_ids:
if account_move_line in invoice.payment_move_line_ids:
account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]})
rec_move_ids += account_move_line.matched_debit_ids rec_move_ids += account_move_line.matched_debit_ids
rec_move_ids += account_move_line.matched_credit_ids rec_move_ids += account_move_line.matched_credit_ids
return rec_move_ids.unlink() return rec_move_ids.unlink()
@@ -45,7 +45,7 @@ class AccountInvoiceReport(models.Model):
user_currency_price_total = fields.Float(string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0) user_currency_price_total = fields.Float(string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0)
price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg") price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg")
user_currency_price_average = fields.Float(string="Average Price", compute='_compute_amounts_in_user_currency', digits=0) user_currency_price_average = fields.Float(string="Average Price", compute='_compute_amounts_in_user_currency', digits=0)
currency_rate = fields.Float(string='Currency Rate', readonly=True) currency_rate = fields.Float(string='Currency Rate', readonly=True, group_operator="avg")
nbr = fields.Integer(string='# of Lines', readonly=True) # TDE FIXME master: rename into nbr_lines nbr = fields.Integer(string='# of Lines', readonly=True) # TDE FIXME master: rename into nbr_lines
type = fields.Selection([ type = fields.Selection([
('out_invoice', 'Customer Invoice'), ('out_invoice', 'Customer Invoice'),
+36
View File
@@ -43,10 +43,46 @@ class TestTax(AccountTestUsers):
(4, self.percent_tax.id, 0) (4, self.percent_tax.id, 0)
] ]
}) })
self.group_tax_bis = self.tax_model.create({
'name': "Group tax bis",
'amount_type': 'group',
'amount': 0,
'sequence': 6,
'children_tax_ids': [
(4, self.fixed_tax.id, 0),
(4, self.percent_tax.id, 0)
]
})
self.group_of_group_tax = self.tax_model.create({
'name': "Group of group tax",
'amount_type': 'group',
'amount': 0,
'sequence': 7,
'children_tax_ids': [
(4, self.group_tax.id, 0),
(4, self.group_tax_bis.id, 0)
]
})
self.bank_journal = self.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', self.account_manager.company_id.id)])[0] self.bank_journal = self.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', self.account_manager.company_id.id)])[0]
self.bank_account = self.bank_journal.default_debit_account_id self.bank_account = self.bank_journal.default_debit_account_id
self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later
def test_tax_group_of_group_tax(self):
self.fixed_tax.include_base_amount = True
self.group_tax.include_base_amount = True
self.group_of_group_tax.include_base_amount = True
res = self.group_of_group_tax.compute_all(200.0)
self.assertEquals(res['total_excluded'], 200.0)
# After calculation of first group
# base = 210
# total_included = 231
# Base of the first grouped is passed
# Base after the second group (220) is dropped.
# Base of the group of groups is passed out,
# so we obtain base as after first group
self.assertEquals(res['base'], 210.0)
self.assertEquals(res['total_included'], 263.0)
def test_tax_group(self): def test_tax_group(self):
res = self.group_tax.compute_all(200.0) res = self.group_tax.compute_all(200.0)
self.assertEquals(res['total_excluded'], 200.0) self.assertEquals(res['total_excluded'], 200.0)
@@ -135,8 +135,8 @@
<field name="user_id"/> <field name="user_id"/>
<field name="date_due"/> <field name="date_due"/>
<field name="origin"/> <field name="origin"/>
<field name="amount_total_signed" string="Total"/> <field name="amount_total_signed" string="Total" sum="Total"/>
<field name="residual_signed" string="Amount Due"/> <field name="residual_signed" string="Amount Due" sum="Amount Due"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="state"/> <field name="state"/>
@@ -200,8 +200,8 @@
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="date_due"/> <field name="date_due"/>
<field name="origin"/> <field name="origin"/>
<field name="amount_total_signed" string="Total"/> <field name="amount_total_signed" string="Total" sum="Total"/>
<field name="residual_signed" string="To Pay"/> <field name="residual_signed" string="To Pay" sum="To pay"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="state"/> <field name="state"/>
@@ -12,7 +12,7 @@
<field name="journal_id"/> <field name="journal_id"/>
<field name="payment_method_id"/> <field name="payment_method_id"/>
<field name="partner_id" string="Customer"/> <field name="partner_id" string="Customer"/>
<field name="amount"/> <field name="amount" sum="Amount"/>
<field name="state"/> <field name="state"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
@@ -31,7 +31,7 @@
<field name="journal_id"/> <field name="journal_id"/>
<field name="payment_method_id"/> <field name="payment_method_id"/>
<field name="partner_id" string="Vendor"/> <field name="partner_id" string="Vendor"/>
<field name="amount"/> <field name="amount" sum="Amount"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="state"/> <field name="state"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
@@ -149,7 +149,7 @@
</p> </p>
</field> </field>
</record> </record>
<record id="action_account_payments_payable" model="ir.actions.act_window"> <record id="action_account_payments_payable" model="ir.actions.act_window">
<field name="name">Payments</field> <field name="name">Payments</field>
<field name="res_model">account.payment</field> <field name="res_model">account.payment</field>
+1 -1
View File
@@ -409,7 +409,7 @@
<span class="fa fa-tasks fa-4x"></span> <span class="fa fa-tasks fa-4x"></span>
<h3><strong>Revenue Recognition</strong></h3> <h3><strong>Revenue Recognition</strong></h3>
<p>Use in conjunction with contracts to calculate your monthly revenue for multi-month contracts.</p> <p>Use in conjunction with contracts to calculate your monthly revenue for multi-month contracts.</p>
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_asset')" <a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_deferred_revenue')"
class="btn odoo_purple"> class="btn odoo_purple">
<span class="fa fa-arrow-circle-o-down"/> Install Now <span class="fa fa-arrow-circle-o-down"/> Install Now
</a> </a>
+12 -2
View File
@@ -139,7 +139,11 @@ var DashBoard = form_common.FormWidget.extend({
}, },
on_close_action: function(e) { on_close_action: function(e) {
if (confirm(_t("Are you sure you want to remove this item ?"))) { if (confirm(_t("Are you sure you want to remove this item ?"))) {
$(e.currentTarget).parents('.oe_action:first').remove(); var $container = $(e.currentTarget).parents('.oe_action:first');
var am = _.findWhere(this.action_managers, { am_id: $container.data('am_id') });
am.destroy();
this.action_managers.splice(_.indexOf(this.action_managers, am), 1);
$container.remove();
this.do_save_dashboard(); this.do_save_dashboard();
} }
}, },
@@ -223,7 +227,13 @@ var DashBoard = form_common.FormWidget.extend({
var am = new ActionManager(this), var am = new ActionManager(this),
// FIXME: ideally the dashboard view shall be refactored like kanban. // FIXME: ideally the dashboard view shall be refactored like kanban.
$action = $('#' + this.view.element_id + '_action_' + index); $action = $('#' + this.view.element_id + '_action_' + index);
$action.parent().data('action_attrs', action_attrs); var $action_container = $action.closest('.oe_action');
var am_id = _.uniqueId('action_manager_');
am.am_id = am_id;
$action_container.data({
action_attrs: action_attrs,
am_id: am_id,
});
this.action_managers.push(am); this.action_managers.push(am);
am.appendTo($action).then(function () { am.appendTo($action).then(function () {
am.do_action(action).then(function () { am.do_action(action).then(function () {
+59 -59
View File
@@ -171,79 +171,71 @@ class HrExpense(models.Model):
''' '''
main function that is called when trying to create the accounting entries related to an expense main function that is called when trying to create the accounting entries related to an expense
''' '''
journal_dict = {}
for expense in self: for expense in self:
acc_date = expense.sheet_id.accounting_date or fields.Date.context_today(self) journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
jrn = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
journal_dict.setdefault(jrn, self.env['hr.expense'])
journal_dict[jrn] |= expense
for journal, expense_list in journal_dict.items():
#create the move that will contain the accounting entries #create the move that will contain the accounting entries
# reference if the same for every expense acc_date = expense.sheet_id.accounting_date or expense.date
ref = len(expense_list.mapped('employee_id.address_home_id.ref')) == 1 and expense_list[0].employee_id.address_home_id.ref or False
move = self.env['account.move'].create({ move = self.env['account.move'].create({
'journal_id': journal.id, 'journal_id': journal.id,
'company_id': self.env.user.company_id.id, 'company_id': self.env.user.company_id.id,
'date': acc_date, 'date': acc_date,
'ref': ref, 'ref': expense.sheet_id.name,
# force the name to the default value, to avoid an eventual 'default_name' in the context # force the name to the default value, to avoid an eventual 'default_name' in the context
# to set it to '' which cause no number to be given to the account.move when posted. # to set it to '' which cause no number to be given to the account.move when posted.
'name': '/', 'name': '/',
}) })
for expense in expense_list: company_currency = expense.company_id.currency_id
company_currency = expense.company_id.currency_id diff_currency_p = expense.currency_id != company_currency
diff_currency_p = expense.currency_id != company_currency #one account.move.line per expense (+taxes..)
#one account.move.line per expense (+taxes..) move_lines = expense._move_line_get()
move_lines = expense._move_line_get()
#create one more move line, a counterline for the total on payable account #create one more move line, a counterline for the total on payable account
payment_id = False payment_id = False
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date) total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
if expense.payment_mode == 'company_account': if expense.payment_mode == 'company_account':
if not expense.sheet_id.bank_journal_id.default_credit_account_id: if not expense.sheet_id.bank_journal_id.default_credit_account_id:
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name)) raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
journal = expense.bank_journal_id journal = expense.sheet_id.bank_journal_id
#create payment #create payment
payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
journal_currency = journal.currency_id or journal.company_id.currency_id journal_currency = journal.currency_id or journal.company_id.currency_id
payment = self.env['account.payment'].create({ payment = self.env['account.payment'].create({
'payment_method_id': payment_methods and payment_methods[0].id or False, 'payment_method_id': payment_methods and payment_methods[0].id or False,
'payment_type': total < 0 and 'outbound' or 'inbound', 'payment_type': total < 0 and 'outbound' or 'inbound',
'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id, 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
'partner_type': 'supplier', 'partner_type': 'supplier',
'journal_id': journal.id, 'journal_id': journal.id,
'payment_date': expense.date, 'payment_date': expense.date,
'state': 'reconciled', 'state': 'reconciled',
'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id, 'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id,
'amount': diff_currency_p and abs(total_currency) or abs(total), 'amount': diff_currency_p and abs(total_currency) or abs(total),
'name': expense.name, 'name': expense.name,
})
payment_id = payment.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
move_lines.append({
'type': 'dest',
'name': expense.employee_id.name,
'price': total,
'account_id': emp_account,
'date_maturity': acc_date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
'payment_id': payment_id,
}) })
payment_id = payment.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
move_lines.append({ #convert eml into an osv-valid format
'type': 'dest', lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines)
'name': expense.employee_id.name, move.write({'line_ids': lines})
'price': total, expense.sheet_id.write({'account_move_id': move.id})
'account_id': emp_account,
'date_maturity': acc_date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
'payment_id': payment_id,
})
#convert eml into an osv-valid format
lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines)
move.write({'line_ids': lines})
expense.sheet_id.write({'account_move_id': move.id})
if expense.payment_mode == 'company_account':
expense.sheet_id.paid_expense_sheets()
move.post() move.post()
if expense.payment_mode == 'company_account':
expense.sheet_id.paid_expense_sheets()
return True return True
@api.multi @api.multi
@@ -526,3 +518,11 @@ class HrExpenseSheet(models.Model):
res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)] res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)]
res['context'] = {'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id} res['context'] = {'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id}
return res return res
@api.multi
def action_open_journal_entries(self):
res = self.env['ir.actions.act_window'].for_xml_id('account', 'action_move_journal_line')
refs = [x.name for x in self]
res['domain'] = [('ref', 'in', refs)]
res['context'] = {}
return res
+8 -1
View File
@@ -422,6 +422,13 @@
type="object"> type="object">
<field name="attachment_number" widget="statinfo" string="Documents"/> <field name="attachment_number" widget="statinfo" string="Documents"/>
</button> </button>
<button name="action_open_journal_entries"
class="oe_stat_button"
icon="fa-book"
string="Accounting Entries"
attrs="{'invisible': [('account_move_id', '=', False)]}"
type="object">
</button>
</div> </div>
<div class="oe_title"> <div class="oe_title">
<label for="name" class="oe_edit_only"/> <label for="name" class="oe_edit_only"/>
@@ -444,7 +451,7 @@
<field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}"/> <field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}"/>
<field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}" domain="[('type', 'in', ['cash', 'bank'])]"/> <field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}" domain="[('type', 'in', ['cash', 'bank'])]"/>
<field name="accounting_date" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['approve', 'post', 'done'])]}"/> <field name="accounting_date" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['approve', 'post', 'done'])]}"/>
<field name="account_move_id" attrs="{'invisible': [('state', 'not in', ['post', 'done'])]}" groups="account.group_account_user" readonly="1"/> <field name="account_move_id" invisible="1"/>
</group> </group>
</group> </group>
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'no_delete': True}"> <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'no_delete': True}">
@@ -605,7 +605,7 @@
<field name="account_id" ref="p112341"/> <field name="account_id" ref="p112341"/>
<field name="refund_account_id" ref="p20011"/> <field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/> <field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_25'), ref('st_tag_in_63')])]"/> <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_25')])]"/>
</record> </record>
<record id="sale_edu_cess_service_tax_2" model="account.tax.template"> <record id="sale_edu_cess_service_tax_2" model="account.tax.template">
<field name="name">(standard)Input Education Cess @ 2% On Service Tax</field> <field name="name">(standard)Input Education Cess @ 2% On Service Tax</field>
@@ -665,7 +665,7 @@
<field name="account_id" ref="p11232"/> <field name="account_id" ref="p11232"/>
<field name="refund_account_id" ref="p20011"/> <field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/> <field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_33'), ref('st_tag_in_86')])]"/> <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_33'), ref('st_tag_in_74')])]"/>
</record> </record>
<!--output purchase tax--> <!--output purchase tax-->
@@ -679,7 +679,7 @@
<field name="account_id" ref="p10050"/> <field name="account_id" ref="p10050"/>
<field name="refund_account_id" ref="p2107"/> <field name="refund_account_id" ref="p2107"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/> <field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_71'), ref('st_tag_in_22')])]"/> <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_22'), ref('st_tag_in_67')])]"/>
</record> </record>
<record id="output_tax_12" model="account.tax.template"> <record id="output_tax_12" model="account.tax.template">
<field name="name">(standard)Output Purchase Tax @ 12%</field> <field name="name">(standard)Output Purchase Tax @ 12%</field>
@@ -691,7 +691,7 @@
<field name="account_id" ref="p10050"/> <field name="account_id" ref="p10050"/>
<field name="refund_account_id" ref="p2107"/> <field name="refund_account_id" ref="p2107"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/> <field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_19'), ref('st_tag_in_67')])]"/> <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_19'), ref('st_tag_in_64')])]"/>
</record> </record>
<record id="output_tax_4" model="account.tax.template"> <record id="output_tax_4" model="account.tax.template">
<field name="name">(standard)Output Purchase Tax @ 4%</field> <field name="name">(standard)Output Purchase Tax @ 4%</field>
+9 -1
View File
@@ -9,6 +9,7 @@ var utils = require('mail.utils');
var config = require('web.config'); var config = require('web.config');
var core = require('web.core'); var core = require('web.core');
var form_common = require('web.form_common'); var form_common = require('web.form_common');
var framework = require('web.framework');
var web_utils = require('web.utils'); var web_utils = require('web.utils');
var _t = core._t; var _t = core._t;
@@ -430,7 +431,14 @@ var Chatter = form_common.AbstractField.extend({
}, },
load_more_messages: function () { load_more_messages: function () {
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}); var self = this;
var top_msg_id = this.$('.o_thread_message').first().data('messageId');
var top_msg_selector = '.o_thread_message[data-message-id="' + top_msg_id + '"]';
var offset = -framework.getPosition(document.querySelector(top_msg_selector)).top;
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}).then(function(){
offset += framework.getPosition(document.querySelector(top_msg_selector)).top;
self.thread.scroll_to({offset: offset});
});
}, },
/** /**
+14 -5
View File
@@ -170,21 +170,31 @@ class StockMove(models.Model):
''' Creates an extra move if necessary depending on extra quantities than foreseen or extra moves''' ''' Creates an extra move if necessary depending on extra quantities than foreseen or extra moves'''
self.ensure_one() self.ensure_one()
quantity_to_split = 0 quantity_to_split = 0
uom_qty_to_split = 0
extra_move = self.env['stock.move'] extra_move = self.env['stock.move']
rounding = self.product_uom.rounding rounding = self.product_uom.rounding
link_procurement = False link_procurement = False
# If more produced than the procurement linked, you should create an extra move
if self.procurement_id and self.production_id and float_compare(self.production_id.qty_produced, self.procurement_id.product_qty, precision_rounding=rounding) > 0: if self.procurement_id and self.production_id and float_compare(self.production_id.qty_produced, self.procurement_id.product_qty, precision_rounding=rounding) > 0:
done_moves_total = sum(self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state == 'done').mapped('product_uom_qty')) done_moves_total = sum(self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state == 'done').mapped('product_uom_qty'))
# If you depassed the quantity before, you don't need to split anymore, but adapt the quantities
if float_compare(done_moves_total, self.procurement_id.product_qty, precision_rounding=rounding) >= 0: if float_compare(done_moves_total, self.procurement_id.product_qty, precision_rounding=rounding) >= 0:
quantity_to_split = 0 quantity_to_split = 0
self.product_uom_qty = self.quantity_done #TODO: could change qty on move_dest_id also (in case of 2-step in/out) if float_compare(self.product_uom_qty, self.quantity_done, precision_rounding=rounding) < 0:
self.product_uom_qty = self.quantity_done #TODO: could change qty on move_dest_id also (in case of 2-step in/out)
else: else:
quantity_to_split = done_moves_total + self.quantity_done - self.procurement_id.product_qty quantity_to_split = done_moves_total + self.quantity_done - self.procurement_id.product_qty
uom_qty_to_split = self.product_uom_qty - (self.quantity_done - quantity_to_split)#self.product_uom_qty - (self.procurement_id.product_qty + done_moves_total)
if float_compare(uom_qty_to_split, quantity_to_split, precision_rounding=rounding) < 0:
uom_qty_to_split = quantity_to_split
self.product_uom_qty = self.quantity_done - quantity_to_split
# You split also simply when the quantity done is bigger than foreseen
elif float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) > 0: elif float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) > 0:
quantity_to_split = self.quantity_done - self.product_uom_qty quantity_to_split = self.quantity_done - self.product_uom_qty
uom_qty_to_split = quantity_to_split # + no need to change existing self.product_uom_qty
link_procurement = True link_procurement = True
if quantity_to_split: if quantity_to_split:
extra_move = self.copy(default={'quantity_done': quantity_to_split, 'product_uom_qty': quantity_to_split, 'production_id': self.production_id.id, extra_move = self.copy(default={'quantity_done': quantity_to_split, 'product_uom_qty': uom_qty_to_split, 'production_id': self.production_id.id,
'raw_material_production_id': self.raw_material_production_id.id, 'raw_material_production_id': self.raw_material_production_id.id,
'procurement_id': link_procurement and self.procurement_id.id or False}) 'procurement_id': link_procurement and self.procurement_id.id or False})
extra_move.action_confirm() extra_move.action_confirm()
@@ -203,8 +213,7 @@ class StockMove(models.Model):
else: else:
movelot.move_id = extra_move.id movelot.move_id = extra_move.id
else: else:
self.quantity_done = self.product_uom_qty self.quantity_done -= quantity_to_split
self.product_uom_qty = self.quantity_done - quantity_to_split
return extra_move return extra_move
@api.multi @api.multi
+2 -2
View File
@@ -118,8 +118,8 @@ class StockWarehouse(models.Model):
return routes return routes
@api.multi @api.multi
def _handle_renaming(self, name, code): def _update_name_and_code(self, name, code):
res = super(StockWarehouse, self)._handle_renaming(name, code) res = super(StockWarehouse, self)._update_name_and_code(name, code)
# change the manufacture procurement rule name # change the manufacture procurement rule name
for warehouse in self: for warehouse in self:
if warehouse.manufacture_pull_id: if warehouse.manufacture_pull_id:
+62 -28
View File
@@ -214,8 +214,10 @@ class PosOrder(models.Model):
partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False
if move is None: if move is None:
# Create an entry for the sale # Create an entry for the sale
journal_id = self.env['ir.config_parameter'].sudo().get_param(
'pos.closing.journal_id', default=order.sale_journal.id)
move = self._create_account_move( move = self._create_account_move(
order.session_id.start_at, order.name, order.sale_journal.id, order.company_id.id) order.session_id.start_at, order.name, int(journal_id), order.company_id.id)
def insert_data(data_type, values): def insert_data(data_type, values):
# if have_to_group_by: # if have_to_group_by:
@@ -540,7 +542,10 @@ class PosOrder(models.Model):
for order in self: for order in self:
address = order.partner_id.address_get(['delivery']) or {} address = order.partner_id.address_get(['delivery']) or {}
picking_type = order.picking_type_id picking_type = order.picking_type_id
picking_id = False return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id
order_picking = Picking
return_picking = Picking
moves = Move
location_id = order.location_id.id location_id = order.location_id.id
if order.partner_id: if order.partner_id:
destination_id = order.partner_id.property_stock_customer.id destination_id = order.partner_id.property_stock_customer.id
@@ -550,9 +555,10 @@ class PosOrder(models.Model):
destination_id = customerloc.id destination_id = customerloc.id
else: else:
destination_id = picking_type.default_location_dest_id.id destination_id = picking_type.default_location_dest_id.id
if picking_type: if picking_type:
pos_qty = all([x.qty >= 0 for x in order.lines]) message = _("This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name)
picking_id = Picking.create({ picking_vals = {
'origin': order.name, 'origin': order.name,
'partner_id': address.get('delivery', False), 'partner_id': address.get('delivery', False),
'date_done': order.date_order, 'date_done': order.date_order,
@@ -560,44 +566,72 @@ class PosOrder(models.Model):
'company_id': order.company_id.id, 'company_id': order.company_id.id,
'move_type': 'direct', 'move_type': 'direct',
'note': order.note or "", 'note': order.note or "",
'location_id': location_id if pos_qty else destination_id, 'location_id': location_id,
'location_dest_id': destination_id if pos_qty else location_id, 'location_dest_id': destination_id,
}) }
message = _("This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name) pos_qty = any([x.qty >= 0 for x in order.lines])
picking_id.message_post(body=message) if pos_qty:
order.write({'picking_id': picking_id.id}) order_picking = Picking.create(picking_vals.copy())
order_picking.message_post(body=message)
neg_qty = any([x.qty < 0 for x in order.lines])
if neg_qty:
return_vals = picking_vals.copy()
return_vals.update({
'location_id': destination_id,
'location_dest_id': return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
'picking_type_id': return_pick_type.id
})
return_picking = Picking.create(return_vals)
return_picking.message_post(body=message)
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']):
Move += Move.create({ moves |= Move.create({
'name': line.name, 'name': line.name,
'product_uom': line.product_id.uom_id.id, 'product_uom': line.product_id.uom_id.id,
'picking_id': picking_id and picking_id.id or False, 'picking_id': order_picking.id if line.qty >= 0 else return_picking.id,
'picking_type_id': picking_type.id, 'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id,
'product_id': line.product_id.id, 'product_id': line.product_id.id,
'product_uom_qty': abs(line.qty), 'product_uom_qty': abs(line.qty),
'state': 'draft', 'state': 'draft',
'location_id': location_id if line.qty >= 0 else destination_id, 'location_id': location_id if line.qty >= 0 else destination_id,
'location_dest_id': destination_id if line.qty >= 0 else location_id, 'location_dest_id': destination_id if line.qty >= 0 else return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
}) })
if picking_id:
picking_id.action_confirm() # prefer associating the regular order picking, not the return
picking_id.force_assign() order.write({'picking_id': order_picking.id or return_picking.id})
order.set_pack_operation_lot()
picking_id.action_done() if return_picking:
elif Move: order._force_picking_done(return_picking)
Move.action_confirm() if order_picking:
Move.force_assign() order._force_picking_done(order_picking)
Move.action_done()
# when the pos.config has no picking_type_id set only the moves will be created
if moves and not return_picking and not order_picking:
moves.action_confirm()
moves.force_assign()
moves.action_done()
return True return True
def set_pack_operation_lot(self): def _force_picking_done(self, picking):
"""Force picking in order to be set as done."""
self.ensure_one()
picking.action_confirm()
picking.force_assign()
self.set_pack_operation_lot(picking)
picking.action_done()
def set_pack_operation_lot(self, picking=None):
"""Set Serial/Lot number in pack operations to mark the pack operation done.""" """Set Serial/Lot number in pack operations to mark the pack operation done."""
StockProductionLot = self.env['stock.production.lot'] StockProductionLot = self.env['stock.production.lot']
PosPackOperationLot = self.env['pos.pack.operation.lot'] PosPackOperationLot = self.env['pos.pack.operation.lot']
if not picking:
picking = order.picking_id
for order in self: for order in self:
for pack_operation in order.picking_id.pack_operation_ids: for pack_operation in picking.pack_operation_ids:
qty = 0 qty = 0
qty_done = 0 qty_done = 0
pack_lots = [] pack_lots = []
@@ -868,15 +902,15 @@ class ReportSaleDetails(models.AbstractModel):
'payments': payments, 'payments': payments,
'company_name': self.env.user.company_id.name, 'company_name': self.env.user.company_id.name,
'taxes': taxes.values(), 'taxes': taxes.values(),
'products': [{ 'products': sorted([{
'product_id': product.id, 'product_id': product.id,
'product_name': product.name[:20], 'product_name': product.name,
'code': product.default_code, 'code': product.default_code,
'quantity': qty, 'quantity': qty,
'price_unit': price_unit, 'price_unit': price_unit,
'discount': discount, 'discount': discount,
'uom': product.uom_id.name 'uom': product.uom_id.name
} for (product, price_unit, discount), qty in products_sold.items()] } for (product, price_unit, discount), qty in products_sold.items()], key=lambda l: l['product_name'])
} }
@api.multi @api.multi
+3 -1
View File
@@ -18,7 +18,9 @@ class PosSession(models.Model):
for session in self: for session in self:
company_id = session.config_id.journal_id.company_id.id company_id = session.config_id.journal_id.company_id.id
orders = session.order_ids.filtered(lambda order: order.state == 'paid') orders = session.order_ids.filtered(lambda order: order.state == 'paid')
move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, session.config_id.journal_id.id, company_id) journal_id = self.env['ir.config_parameter'].sudo().get_param(
'pos.closing.journal_id', default=session.config_id.journal_id.id)
move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id)
orders.with_context(force_company=company_id)._create_account_move_line(session, move) orders.with_context(force_company=company_id)._create_account_move_line(session, move)
for order in session.order_ids.filtered(lambda o: o.state != 'done'): for order in session.order_ids.filtered(lambda o: o.state != 'done'):
if order.state not in ('paid', 'invoiced'): if order.state not in ('paid', 'invoiced'):
+1 -1
View File
@@ -871,7 +871,7 @@
<div class='orderlines' line-ratio='0.6'> <div class='orderlines' line-ratio='0.6'>
<t t-foreach='products' t-as='line'> <t t-foreach='products' t-as='line'>
<line> <line>
<left><t t-esc='line.product_name' /></left> <left><t t-esc='line.product_name.substr(0,20)' /></left>
<right> <right>
<value value-decimals='2' value-autoint='on'> <value value-decimals='2' value-autoint='on'>
<t t-esc='line.quantity' /> <t t-esc='line.quantity' />
@@ -119,9 +119,208 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# I test that the generated journal entry is attached to the PoS order # I test that the generated journal entry is attached to the PoS order
self.assertTrue(self.pos_order_pos0.account_move, "Journal entry has not been attached to Pos order.") self.assertTrue(self.pos_order_pos0.account_move, "Journal entry has not been attached to Pos order.")
def test_order_to_picking(self):
"""
In order to test the Point of Sale in module, I will do three orders from the sale to the payment,
invoicing + picking, but will only check the picking consistency in the end.
TODO: Check the negative picking after changing the picking relation to One2many (also for a mixed use case),
check the quantity, the locations and return picking logic
"""
# I click on create a new session button
self.pos_config.open_session_cb()
# I create a PoS order with 2 units of PCSC234 at 450 EUR
# and 3 units of PCSC349 at 300 EUR.
self.pos_order_pos1 = self.PosOrder.create({
'company_id': self.company_id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'partner_id': self.partner1.id,
'lines': [(0, 0, {
'name': "OL/0001",
'product_id': self.product3.id,
'price_unit': 450,
'discount': 0.0,
'qty': 2.0,
}), (0, 0, {
'name': "OL/0002",
'product_id': self.product4.id,
'price_unit': 300,
'discount': 0.0,
'qty': 3.0,
})]
})
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (2*450 + 3*300 = 1800)
context_make_payment = {
"active_ids": [self.pos_order_pos1.id],
"active_id": self.pos_order_pos1.id
}
self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({
'amount': 1800
})
# I click on the validate button to register the payment.
context_payment = {'active_id': self.pos_order_pos1.id}
self.pos_make_payment_2.with_context(context_payment).check()
# I check that the order is marked as paid
self.assertEqual(
self.pos_order_pos1.state,
'paid',
'Order should be in paid state.'
)
# I generate the journal entries
self.pos_order_pos1._create_account_move_line()
# I test that the generated journal entry is attached to the PoS order
self.assertTrue(
self.pos_order_pos1.account_move,
"Journal entry has not been attached to Pos order."
)
# I test that the pickings are created as expected
# One picking attached and having all the positive move lines in the correct state
self.pos_order_pos1.create_picking()
self.assertEqual(
self.pos_order_pos1.picking_id.state,
'done',
'Picking should be in done state.'
)
self.assertEqual(
self.pos_order_pos1.picking_id.move_lines.mapped('state'),
['done', 'done'],
'Move Lines should be in done state.'
)
self.pos_order_pos2 = self.PosOrder.create({
'company_id': self.company_id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'partner_id': self.partner1.id,
'lines': [(0, 0, {
'name': "OL/0003",
'product_id': self.product3.id,
'price_unit': 450,
'discount': 0.0,
'qty': (-2.0),
}), (0, 0, {
'name': "OL/0004",
'product_id': self.product4.id,
'price_unit': 300,
'discount': 0.0,
'qty': (-3.0),
})]
})
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + -3*300 = -1800)
context_make_payment = {
"active_ids": [self.pos_order_pos2.id],
"active_id": self.pos_order_pos2.id
}
self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({
'amount': (-1800)
})
# I click on the validate button to register the payment.
context_payment = {'active_id': self.pos_order_pos2.id}
self.pos_make_payment_3.with_context(context_payment).check()
# I check that the order is marked as paid
self.assertEqual(
self.pos_order_pos2.state,
'paid',
'Order should be in paid state.'
)
# I generate the journal entries
self.pos_order_pos2._create_account_move_line()
# I test that the generated journal entry is attached to the PoS order
self.assertTrue(
self.pos_order_pos2.account_move,
"Journal entry has not been attached to PoS order."
)
# I test that the pickings are created as expected
# One picking attached and having all the positive move lines in the correct state
self.pos_order_pos2.create_picking()
self.assertEqual(
self.pos_order_pos2.picking_id.state,
'done',
'Picking should be in done state.'
)
self.assertEqual(
self.pos_order_pos2.picking_id.move_lines.mapped('state'),
['done', 'done'],
'Move Lines should be in done state.'
)
self.pos_order_pos3 = self.PosOrder.create({
'company_id': self.company_id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'partner_id': self.partner1.id,
'lines': [(0, 0, {
'name': "OL/0005",
'product_id': self.product3.id,
'price_unit': 450,
'discount': 0.0,
'qty': (-2.0),
}), (0, 0, {
'name': "OL/0006",
'product_id': self.product4.id,
'price_unit': 300,
'discount': 0.0,
'qty': 3.0,
})]
})
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + 3*300 = 0)
context_make_payment = {
"active_ids": [self.pos_order_pos3.id],
"active_id": self.pos_order_pos3.id
}
self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({
'amount': 0
})
# I click on the validate button to register the payment.
context_payment = {'active_id': self.pos_order_pos3.id}
self.pos_make_payment_4.with_context(context_payment).check()
# I check that the order is marked as paid
self.assertEqual(
self.pos_order_pos3.state,
'paid',
'Order should be in paid state.'
)
# I generate the journal entries
self.pos_order_pos3._create_account_move_line()
# I test that the generated journal entry is attached to the PoS order
self.assertTrue(
self.pos_order_pos3.account_move,
"Journal entry has not been attached to PoS order."
)
# I test that the pickings are created as expected
# One picking attached and having all the positive move lines in the correct state
self.pos_order_pos3.create_picking()
self.assertEqual(
self.pos_order_pos3.picking_id.state,
'done',
'Picking should be in done state.'
)
self.assertEqual(
self.pos_order_pos3.picking_id.move_lines.mapped('state'),
['done'],
'Move Lines should be in done state.'
)
def test_order_to_invoice(self): def test_order_to_invoice(self):
#I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
self.pos_order_pos1 = self.PosOrder.create({ self.pos_order_pos1 = self.PosOrder.create({
'company_id': self.company_id, 'company_id': self.company_id,
'partner_id': self.partner1.id, 'partner_id': self.partner1.id,
+1 -2
View File
@@ -392,8 +392,7 @@ class ProductProduct(models.Model):
name = variant and "%s (%s)" % (product.name, variant) or product.name name = variant and "%s (%s)" % (product.name, variant) or product.name
sellers = [] sellers = []
if partner_ids: if partner_ids:
if variant: sellers = [x for x in product.seller_ids if (x.name.id in partner_ids) and (x.product_id == product)]
sellers = [x for x in product.seller_ids if (x.name.id in partner_ids) and (x.product_id == product)]
if not sellers: if not sellers:
sellers = [x for x in product.seller_ids if (x.name.id in partner_ids) and not x.product_id] sellers = [x for x in product.seller_ids if (x.name.id in partner_ids) and not x.product_id]
if sellers: if sellers:
+4 -4
View File
@@ -124,8 +124,8 @@ class StockWarehouse(models.Model):
return routes return routes
@api.multi @api.multi
def _handle_renaming(self, name, code): def _update_name_and_code(self, name, code):
res = super(StockWarehouse, self)._handle_renaming(name, code) res = super(StockWarehouse, self)._update_name_and_code(name, code)
warehouse = self[0] warehouse = self[0]
#change the buy procurement rule name #change the buy procurement rule name
if warehouse.buy_pull_id: if warehouse.buy_pull_id:
@@ -133,8 +133,8 @@ class StockWarehouse(models.Model):
return res return res
@api.multi @api.multi
def change_route(self): def _update_routes(self):
res = super(StockWarehouse, self).change_route() res = super(StockWarehouse, self)._update_routes()
for warehouse in self: for warehouse in self:
if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id: if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id:
warehouse.buy_pull_id.write({'location_id': warehouse.in_type_id.default_location_dest_id.id}) warehouse.buy_pull_id.write({'location_id': warehouse.in_type_id.default_location_dest_id.id})
+1 -1
View File
@@ -653,7 +653,7 @@ class ResourceCalendarAttendance(models.Model):
date_to = fields.Date(string='End Date') date_to = fields.Date(string='End Date')
hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.") hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.")
hour_to = fields.Float(string='Work to', required=True) hour_to = fields.Float(string='Work to', required=True)
calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True) calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
def hours_time_string(hours): def hours_time_string(hours):
+1 -1
View File
@@ -175,7 +175,7 @@ class SaleOrder(models.Model):
return super(SaleOrder, self)._track_subtype(init_values) return super(SaleOrder, self)._track_subtype(init_values)
@api.multi @api.multi
@api.onchange('partner_shipping_id') @api.onchange('partner_shipping_id', 'partner_id')
def onchange_partner_shipping_id(self): def onchange_partner_shipping_id(self):
""" """
Trigger the change of fiscal position when the shipping address is modified. Trigger the change of fiscal position when the shipping address is modified.
@@ -67,9 +67,8 @@ class SaleAdvancePaymentInv(models.TransientModel):
if self.product_id.id: if self.product_id.id:
account_id = self.product_id.property_account_income_id.id account_id = self.product_id.property_account_income_id.id
if not account_id: if not account_id:
prop = ir_property_obj.get('property_account_income_categ_id', 'product.category') inc_acc = ir_property_obj.get('property_account_income_categ_id', 'product.category')
prop_id = prop and prop.id or False account_id = order.fiscal_position_id.map_account(inc_acc).id if inc_acc else False
account_id = order.fiscal_position_id.map_account(prop_id)
if not account_id: if not account_id:
raise UserError( raise UserError(
_('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.') % _('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.') %
+8 -2
View File
@@ -10,6 +10,9 @@ from odoo import api, fields, models, registry, _
from odoo.osv import expression from odoo.osv import expression
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round
import logging
_logger = logging.getLogger(__name__)
class ProcurementGroup(models.Model): class ProcurementGroup(models.Model):
_inherit = 'procurement.group' _inherit = 'procurement.group'
@@ -172,8 +175,11 @@ class ProcurementOrder(models.Model):
'propagate': self.rule_id.propagate, 'propagate': self.rule_id.propagate,
'priority': self.priority, 'priority': self.priority,
} }
# compatibility
_run_move_create = _get_stock_move_values def _run_move_create(self):
# FIXME - remove me in master/saas-14
_logger.warning("'_run_move_create' has been renamed into '_get_stock_move_values'... Overrides are ignored")
return self._get_stock_move_values()
@api.multi @api.multi
def _run(self): def _run(self):
+21 -4
View File
@@ -6,6 +6,10 @@ from odoo.tools.translate import _
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.exceptions import UserError from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class Quant(models.Model): class Quant(models.Model):
""" Quants are the smallest unit of stock physical instances """ """ Quants are the smallest unit of stock physical instances """
@@ -261,8 +265,17 @@ class Quant(models.Model):
# create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants # create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
return self.sudo().create(vals) return self.sudo().create(vals)
# compatibility method
_quant_create = _quant_create_from_move @api.model
def _quant_create(self, qty, move, lot_id=False, owner_id=False,
src_package_id=False, dest_package_id=False,
force_location_from=False, force_location_to=False):
# FIXME - remove me in master/saas-14
_logger.warning("'_quant_create' has been renamed into '_quant_create_from_move'... Overrides are ignored")
return self._quant_create_from_move(
qty, move, lot_id=lot_id, owner_id=owner_id,
src_package_id=src_package_id, dest_package_id=dest_package_id,
force_location_from=force_location_from, force_location_to=force_location_to)
@api.multi @api.multi
def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False): def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
@@ -275,8 +288,12 @@ class Quant(models.Model):
if not entire_pack: if not entire_pack:
vals.update({'package_id': dest_package_id}) vals.update({'package_id': dest_package_id})
self.write(vals) self.write(vals)
# compatibility method
move_quants_write = _quant_update_from_move @api.multi
def move_quants_write(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
# FIXME - remove me in master/saas-14
_logger.warning("'move_quants_write' has been renamed into '_quant_update_from_move'... Overrides are ignored")
return self._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
@api.one @api.one
def _quant_reconcile_negative(self, move): def _quant_reconcile_negative(self, move):
+38 -8
View File
@@ -10,6 +10,10 @@ from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError from odoo.exceptions import UserError, ValidationError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
import logging
_logger = logging.getLogger(__name__)
class Warehouse(models.Model): class Warehouse(models.Model):
_name = "stock.warehouse" _name = "stock.warehouse"
@@ -452,7 +456,11 @@ class Warehouse(models.Model):
'product_categ_selectable': True, 'product_categ_selectable': True,
'supplied_wh_id': self.id, 'supplied_wh_id': self.id,
'supplier_wh_id': supplier_warehouse.id} 'supplier_wh_id': supplier_warehouse.id}
_get_inter_wh_route = _get_inter_warehouse_route_values # compatibility
def _get_inter_wh_route(self, supplier_warehouse):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_inter_wh_route' has been renamed into '_get_inter_warehouse_route_values'... Overrides are ignored")
return self._get_inter_warehouse_route_values(supplier_warehouse)
def _get_crossdock_route_values(self): def _get_crossdock_route_values(self):
return { return {
@@ -463,8 +471,10 @@ class Warehouse(models.Model):
'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step',
'sequence': 20} 'sequence': 20}
def _get_crossdock_route(self, route_name): # compatibility def _get_crossdock_route(self, route_name):
return self._get_crossdock_route_values() # FIXME - remove me in master/saas-14
_logger.warning("'_get_crossdock_route' has been renamed into '_get_crossdock_route_values'... Overrides are ignored")
return self._get_crossdock_route_values(route_name)
# Pull / Push tools # Pull / Push tools
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -503,9 +513,15 @@ class Warehouse(models.Model):
'procure_method': 'make_to_order', 'procure_method': 'make_to_order',
'active': True}, name_suffix=_('MTO')) 'active': True}, name_suffix=_('MTO'))
return pull_rules_list return pull_rules_list
_get_mto_pull_rule = _get_mto_pull_rules_values # compatibility
def _get_push_pull_rules(self, active, values, new_route_id): # compatibility def _get_mto_pull_rule(self, route_values):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_mto_pull_rule' has been renamed into '_get_mto_pull_rules_values'... Overrides are ignored")
return self._get_mto_pull_rules_values(route_values)
def _get_push_pull_rules(self, active, values, new_route_id):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_push_pull_rules' has been renamed into '_get_push_pull_rules_values'... Overrides are ignored")
return self._get_push_pull_rules_values(values, values={'active': active, 'route_id': new_route_id}) return self._get_push_pull_rules_values(values, values={'active': active, 'route_id': new_route_id})
def _get_supply_pull_rules_values(self, route_values, values=None): def _get_supply_pull_rules_values(self, route_values, values=None):
@@ -523,7 +539,11 @@ class Warehouse(models.Model):
if delivery_new and warehouse.delivery_steps != delivery_new and (warehouse.delivery_steps == 'ship_only' or delivery_new == 'ship_only'): if delivery_new and warehouse.delivery_steps != delivery_new and (warehouse.delivery_steps == 'ship_only' or delivery_new == 'ship_only'):
change_to_multiple = warehouse.delivery_steps == 'ship_only' change_to_multiple = warehouse.delivery_steps == 'ship_only'
warehouse._check_delivery_resupply(output_loc, change_to_multiple) warehouse._check_delivery_resupply(output_loc, change_to_multiple)
_check_resupply = _update_reception_delivery_resupply
def _check_resupply(self, reception_new, delivery_new):
# FIXME - remove me in master/saas-14
_logger.warning("'_check_resupply' has been renamed into '_update_reception_delivery_resupply'... Overrides are ignored")
return self._update_reception_delivery_resupply(reception_new, delivery_new)
def _check_delivery_resupply(self, new_location, change_to_multiple): def _check_delivery_resupply(self, new_location, change_to_multiple):
""" Check if the resupply routes from this warehouse follow the changes of number of delivery steps """ Check if the resupply routes from this warehouse follow the changes of number of delivery steps
@@ -567,7 +587,12 @@ class Warehouse(models.Model):
self._create_or_update_crossdock_route(routes_data) self._create_or_update_crossdock_route(routes_data)
self._create_or_update_mto_pull(routes_data) self._create_or_update_mto_pull(routes_data)
return True return True
change_route = _update_routes
@api.multi
def change_route(self):
# FIXME - remove me in master/saas-14
_logger.warning("'change_route' has been renamed into '_update_routes'... Overrides are ignored")
return self._update_routes()
@api.one @api.one
def _update_picking_type(self): def _update_picking_type(self):
@@ -597,7 +622,12 @@ class Warehouse(models.Model):
warehouse.pack_type_id.sequence_id.write(sequence_data['pack_type_id']) warehouse.pack_type_id.sequence_id.write(sequence_data['pack_type_id'])
warehouse.pick_type_id.sequence_id.write(sequence_data['pick_type_id']) warehouse.pick_type_id.sequence_id.write(sequence_data['pick_type_id'])
warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id']) warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id'])
_handle_renaming = _update_name_and_code
@api.multi
def _handle_renaming(self, new_name=False, new_code=False):
# FIXME - remove me in master/saas-14
_logger.warning("'_handle_renaming' has been renamed into '_update_name_and_code'... Overrides are ignored")
return self._update_name_and_code(new_name=new_name, new_code=new_code)
def _update_location_reception(self, new_reception_step): def _update_location_reception(self, new_reception_step):
switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id)) switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id))
+6 -5
View File
@@ -793,18 +793,19 @@ var BufferedDataSet = DataSetStatic.extend({
var def = $.Deferred(); var def = $.Deferred();
this.mutex.exec(function () { this.mutex.exec(function () {
var dirty = false; var dirty = false;
_.each(data, function (v, k) { // _.each is broken if a field "length" is present
if (!_.isEqual(v, cached.values[k])) { for (var k in data) {
if (!_.isEqual(data[k], cached.values[k])) {
dirty = true; dirty = true;
if (_.isEqual(v, cached.from_read[k])) { // clean changes if (_.isEqual(data[k], cached.from_read[k])) { // clean changes
delete cached.changes[k]; delete cached.changes[k];
} else { } else {
cached.changes[k] = v; cached.changes[k] = data[k];
} }
} else { } else {
delete data[k]; delete data[k];
} }
}); }
self._update_cache(id, options); self._update_cache(id, options);
if (dirty) { if (dirty) {
+4 -3
View File
@@ -470,11 +470,12 @@ var ListView = View.extend({
self.records.remove(record); self.records.remove(record);
return; return;
} }
_.each(values, function (value, key) { // _.each is broken if a field "length" is present
for (var key in values) {
if (fields[key] && fields[key].type === 'many2many') if (fields[key] && fields[key].type === 'many2many')
record.set(key + '__display', false, {silent: true}); record.set(key + '__display', false, {silent: true});
record.set(key, value, {silent: true}); record.set(key, values[key], {silent: true});
}); }
record.trigger('change', record); record.trigger('change', record);
/* When a record is reloaded, there is a rendering lag because of the addition/suppression of /* When a record is reloaded, there is a rendering lag because of the addition/suppression of
+21
View File
@@ -196,6 +196,13 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView', 'web.data_manag
mock.add('demo:onchange', function () { mock.add('demo:onchange', function () {
return {}; return {};
}); });
mock.add('demo:fields_get', function () {
return {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
};
});
} }
test('newrecord', function (assert, data, ListView, data_manager) { test('newrecord', function (assert, data, ListView, data_manager) {
@@ -263,6 +270,13 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView', 'web.dat
mock.add('demo:read', function () { mock.add('demo:read', function () {
return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }];
}); });
mock.add('demo:fields_get', function () {
return {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
};
});
} }
test('edition events',function (assert, data, ListView, data_manager) { test('edition events',function (assert, data, ListView, data_manager) {
@@ -345,6 +359,13 @@ odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView', 'web.da
mock.add('demo:default_get', function () { return {}; }); mock.add('demo:default_get', function () { return {}; });
mock.add('demo:create', function () { return 1; }); mock.add('demo:create', function () { return 1; });
mock.add('demo:on_write', function () { return [42]; }); mock.add('demo:on_write', function () { return [42]; });
mock.add('demo:fields_get', function () {
return {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
};
});
var ds = new data.DataSetStatic(null, 'demo', null, []); var ds = new data.DataSetStatic(null, 'demo', null, []);
var fields_view = data_manager._postprocess_fvg({ var fields_view = data_manager._postprocess_fvg({
+4 -2
View File
@@ -2,7 +2,7 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manag
test('record-deletion', function (assert, ListView, data, data_manager) { test('record-deletion', function (assert, ListView, data, data_manager) {
assert.expect(2); assert.expect(2);
mock.add('demo:read', function (args, kwargs) { mock.add('demo:read', function (args, kwargs) {
if (_.isEqual(args[0], [1, 2, 3])) { if (_.isEqual(args[0], [1, 2, 3])) {
return [ return [
@@ -18,7 +18,9 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manag
throw new Error(JSON.stringify(_.toArray(arguments))); throw new Error(JSON.stringify(_.toArray(arguments)));
}); });
mock.add('/web/dataset/call_button', function () { return false; }); mock.add('/web/dataset/call_button', function () { return false; });
mock.add('demo:fields_get', function() {
return {a: {type: 'char', string: "A"}};
});
var ds = new data.DataSetStatic(null, 'demo', null, [1, 2, 3]); var ds = new data.DataSetStatic(null, 'demo', null, [1, 2, 3]);
var fields_view = data_manager._postprocess_fvg({ var fields_view = data_manager._postprocess_fvg({
+2 -1
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
import odoo.tests import odoo.tests
class WebSuite(odoo.tests.HttpCase): class WebSuite(odoo.tests.HttpCase):
@unittest.skip('Memory leak in this test lead to phantomjs crash, making it unreliable')
def test_01_js(self): def test_01_js(self):
self.phantom_js('/web/tests?mod=web',"","", login='admin') self.phantom_js('/web/tests?mod=web',"","", login='admin')
+18 -22
View File
@@ -246,16 +246,13 @@ var DiagramView = View.extend({
self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded); self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded);
}); });
var form_fields = [self.parent_field]; pop.opened().then(function(){
var form_controller = pop.view_form; var form_controller = pop.view_form;
var field = form_controller.fields[self.parent_field];
form_controller.on("load_record", self, function(){ if (field) {
_.each(form_fields, function(fld) {
if (!(fld in form_controller.fields)) { return; }
var field = form_controller.fields[fld];
field.$input.prop('disabled', true); field.$input.prop('disabled', true);
field.$dropdown.unbind(); field.$dropdown.unbind();
}); }
}); });
}, },
@@ -274,16 +271,13 @@ var DiagramView = View.extend({
} }
}).open(); }).open();
var form_controller = pop.view_form; pop.opened().then(function(){
var form_fields = [this.parent_field]; var form_controller = pop.view_form;
var field = form_controller.fields[self.parent_field];
form_controller.on("load_record", self, function(){ if (field) {
_.each(form_fields, function(fld) {
if (!(fld in form_controller.fields)) { return; }
var field = form_controller.fields[fld];
field.set_value(self.id); field.set_value(self.id);
field.dirty = true; field.dirty = true;
}); }
}); });
}, },
@@ -326,12 +320,14 @@ var DiagramView = View.extend({
} }
}); });
var form_controller = pop.view_form; pop.opened().then(function(){
form_controller.on("load_record", self, function(){ var form_controller = pop.view_form;
form_controller.fields[self.connectors.attrs.source].set_value(node_source_id); var source_field = form_controller.fields[self.connectors.attrs.source];
form_controller.fields[self.connectors.attrs.source].dirty = true; var destination_field = form_controller.fields[self.connectors.attrs.destination];
form_controller.fields[self.connectors.attrs.destination].set_value(node_dest_id); source_field.set_value(node_source_id);
form_controller.fields[self.connectors.attrs.destination].dirty = true; source_field.dirty = true;
destination_field.set_value(node_dest_id);
destination_field.dirty = true;
}); });
}, },
+2
View File
@@ -67,6 +67,8 @@ class Planner(models.Model):
module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)], limit=1) module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)], limit=1)
if module: if module:
params['id'] = module.id params['id'] = module.id
else:
return "#show_enterprise"
return "/web#%s" % (urlencode(params),) return "/web#%s" % (urlencode(params),)
@api.model @api.model
@@ -49,6 +49,7 @@ var PlannerDialog = Widget.extend({
category_selector: 'div[menu-category-id]', category_selector: 'div[menu-category-id]',
events: { events: {
'click li a[href^="#"]:not([data-toggle="collapse"])': 'change_page', 'click li a[href^="#"]:not([data-toggle="collapse"])': 'change_page',
'click a[href^="#show_enterprise"]': 'show_enterprise',
'click button.mark_as_done': 'click_on_done', 'click button.mark_as_done': 'click_on_done',
'click a.btn-next': 'change_to_next_page', 'click a.btn-next': 'change_to_next_page',
'click .o_planner_close_block span': 'close_modal', 'click .o_planner_close_block span': 'close_modal',
@@ -420,7 +421,40 @@ var PlannerDialog = Widget.extend({
destroy: function() { destroy: function() {
this.$el.modal('hide'); this.$el.modal('hide');
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
} },
show_enterprise: function(ev) {
ev.preventDefault();
var message = $(QWeb.render('EnterpriseUpgrade'));
var buttons = [
{
text: _t("Upgrade now"),
classes: 'btn-primary',
close: true,
click: this.confirm_upgrade,
},
{
text: _t("Cancel"),
close: true,
},
];
var dialog = new Dialog(this, {
size: 'medium',
buttons: buttons,
$content: $('<div>', {
html: message,
}),
title: _t("Odoo Enterprise"),
}).open();
// force dialog to be hover the planner
dialog.$el.parents('.modal').css('z-index', 1052);
return dialog;
},
confirm_upgrade: function() {
new Model("res.users").call("search_count", [[["share", "=", false]]]).then(function(data) {
window.location = "https://www.odoo.com/odoo-enterprise/upgrade?utm_medium=community_upgrade&num_users=" + data;
});
},
}); });
return { return {
+1 -1
View File
@@ -43,7 +43,7 @@ var ViewEditor = AceCommon.ViewEditor.extend({
var args = { var args = {
key: $(document.documentElement).data('view-xmlid'), key: $(document.documentElement).data('view-xmlid'),
full: true, full: true,
bundles: !!$('script[src*=".assets_common"]').length bundles: this.$('.js_include_bundles')[0].checked
}; };
return ajax return ajax
.jsonRpc('/website/customize_template_get', 'call', args) .jsonRpc('/website/customize_template_get', 'call', args)
@@ -259,7 +259,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
// Check if the user is authenticated and has the right to make API calls // Check if the user is authenticated and has the right to make API calls
if (!gapi.analytics.auth.getAuthResponse()) { if (!gapi.analytics.auth.getAuthResponse()) {
this.display_unauthorized_message($analytics_components, 'not_connected'); this.display_unauthorized_message($analytics_components, 'not_connected');
} else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics ') === -1) { } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) {
this.display_unauthorized_message($analytics_components, 'no_right'); this.display_unauthorized_message($analytics_components, 'no_right');
} else { } else {
this.make_analytics_calls($analytics_components); this.make_analytics_calls($analytics_components);
+2 -2
View File
@@ -135,9 +135,9 @@ odoo.define('website.website', function (require) {
var error = function (title, message, url) { var error = function (title, message, url) {
return new Dialog(null, { return new Dialog(null, {
title: data.data ? data.data.arguments[0] : "", title: title || "",
$content: $(qweb.render('website.error_dialog', { $content: $(qweb.render('website.error_dialog', {
message: data.data ? data.data.arguments[1] : data.statusText, message: message || "",
backend_url: url, backend_url: url,
})), })),
}).open(); }).open();
@@ -15,7 +15,8 @@ class MailGroup(http.Controller):
def _get_archives(self, group_id): def _get_archives(self, group_id):
MailMessage = request.env['mail.message'] MailMessage = request.env['mail.message']
groups = MailMessage._read_group_raw( groups = MailMessage._read_group_raw(
[('model', '=', 'mail.channel'), ('res_id', '=', group_id)], ['subject', 'date'], [('model', '=', 'mail.channel'), ('res_id', '=', group_id), ('message_type', '!=', 'notification')],
['subject', 'date'],
groupby=["date"], orderby="date desc") groupby=["date"], orderby="date desc")
for group in groups: for group in groups:
(r, label) = group['date'] (r, label) = group['date']
@@ -41,7 +42,7 @@ class MailGroup(http.Controller):
# compute statistics # compute statistics
month_date = datetime.today() - relativedelta.relativedelta(months=1) month_date = datetime.today() - relativedelta.relativedelta(months=1)
messages = request.env['mail.message'].read_group( messages = request.env['mail.message'].read_group(
[('model', '=', 'mail.channel'), ('date', '>=', fields.Datetime.to_string(month_date))], [('model', '=', 'mail.channel'), ('date', '>=', fields.Datetime.to_string(month_date)), ('message_type', '!=', 'notification')],
[], ['res_id']) [], ['res_id'])
message_data = dict((message['res_id'], message['res_id_count']) for message in messages) message_data = dict((message['res_id'], message['res_id_count']) for message in messages)
+1 -1
View File
@@ -17,6 +17,7 @@ Depends:
node-less, node-less,
postgresql-client, postgresql-client,
python, python,
python-babel,
python-dateutil, python-dateutil,
python-decorator, python-decorator,
python-docutils, python-docutils,
@@ -32,7 +33,6 @@ Depends:
python-passlib, python-passlib,
python-psutil, python-psutil,
python-psycopg2, python-psycopg2,
python-pybabel,
python-pychart, python-pychart,
python-pydot, python-pydot,
python-pyparsing, python-pyparsing,
+21
View File
@@ -131,6 +131,10 @@ class IrModel(models.Model):
if model.state != 'manual': if model.state != 'manual':
raise UserError(_("Model '%s' contains module data and cannot be removed!") % model.name) raise UserError(_("Model '%s' contains module data and cannot be removed!") % model.name)
# prevent screwing up fields that depend on these models' fields
for model in self:
model.field_id._prepare_update()
self._drop_table() self._drop_table()
res = super(IrModel, self).unlink() res = super(IrModel, self).unlink()
@@ -421,6 +425,19 @@ class IrModelFields(models.Model):
return True return True
@api.multi
def _prepare_update(self):
""" Check whether the fields in ``self`` may be modified or removed.
This method prevents the modification/deletion of many2one fields
that have an inverse one2many, for instance.
"""
for record in self:
model = self.env[record.model]
field = model._fields[record.name]
if field.type == 'many2one' and model._field_inverses.get(field):
msg = _("The field '%s' cannot be removed because the field '%s' depends on it.")
raise UserError(msg % (field, model._field_inverses[field][0]))
@api.multi @api.multi
def unlink(self): def unlink(self):
if not self: if not self:
@@ -431,6 +448,9 @@ class IrModelFields(models.Model):
any(field.state != 'manual' for field in self): any(field.state != 'manual' for field in self):
raise UserError(_("This column contains module data and cannot be removed!")) raise UserError(_("This column contains module data and cannot be removed!"))
# prevent screwing up fields that depend on these fields
self._prepare_update()
model_names = self.mapped('model') model_names = self.mapped('model')
self._drop_column() self._drop_column()
res = super(IrModelFields, self).unlink() res = super(IrModelFields, self).unlink()
@@ -522,6 +542,7 @@ class IrModelFields(models.Model):
if vals.get('name', item.name) != item.name: if vals.get('name', item.name) != item.name:
# We need to rename the column # We need to rename the column
item._prepare_update()
if column_rename: if column_rename:
raise UserError(_('Can only rename one field at a time!')) raise UserError(_('Can only rename one field at a time!'))
if vals['name'] in obj._fields: if vals['name'] in obj._fields:
+7 -2
View File
@@ -355,7 +355,7 @@ class HttpCase(TransactionCase):
_logger.info("phantomjs: %s", line) _logger.info("phantomjs: %s", line)
if line == "ok": if line == "ok":
break return True
def phantom_run(self, cmd, timeout): def phantom_run(self, cmd, timeout):
_logger.info('phantom_run executing %s', ' '.join(cmd)) _logger.info('phantom_run executing %s', ' '.join(cmd))
@@ -369,8 +369,9 @@ class HttpCase(TransactionCase):
phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None) phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None)
except OSError: except OSError:
raise unittest.SkipTest("PhantomJS not found") raise unittest.SkipTest("PhantomJS not found")
result = False
try: try:
self.phantom_poll(phantom, timeout) result = self.phantom_poll(phantom, timeout)
finally: finally:
# kill phantomjs if phantom.exit() wasn't called in the test # kill phantomjs if phantom.exit() wasn't called in the test
if phantom.poll() is None: if phantom.poll() is None:
@@ -379,6 +380,10 @@ class HttpCase(TransactionCase):
self._wait_remaining_requests() self._wait_remaining_requests()
# we ignore phantomjs return code as we kill it as soon as we have ok # we ignore phantomjs return code as we kill it as soon as we have ok
_logger.info("phantom_run execution finished") _logger.info("phantom_run execution finished")
self.assertTrue(
result,
"PhantomJS test completed without reporting success; "
"the log may contain errors or hints.")
def _wait_remaining_requests(self): def _wait_remaining_requests(self):
t0 = int(time.time()) t0 = int(time.time())
+13 -5
View File
@@ -111,7 +111,7 @@ class configmanager(object):
group.add_option("--addons-path", dest="addons_path", group.add_option("--addons-path", dest="addons_path",
help="specify additional addons paths (separated by commas).", help="specify additional addons paths (separated by commas).",
action="callback", callback=self._check_addons_path, nargs=1, type="string") action="callback", callback=self._check_addons_path, nargs=1, type="string")
group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web") group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules. Default is 'web,web_kanban'")
group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(), group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(),
help="Directory where to store Odoo data") help="Directory where to store Odoo data")
@@ -406,6 +406,7 @@ class configmanager(object):
'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'test_file', 'test_enable', 'test_commit', 'test_report_directory',
'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent',
'data_dir', 'data_dir',
'server_wide_modules',
] ]
posix_keys = [ posix_keys = [
@@ -459,14 +460,21 @@ class configmanager(object):
if len(self.options['language']) > 5: if len(self.options['language']) > 5:
raise Exception('ERROR: The Lang name must take max 5 chars, Eg: -lfr_BE') raise Exception('ERROR: The Lang name must take max 5 chars, Eg: -lfr_BE')
# server_wide_modules defaults to web,web_kanban if empty or unset
server_wide_modules = self.options['server_wide_modules'] = (
self.options['server_wide_modules']
if self.options['server_wide_modules']
else 'web,web_kanban'
)
if opt.save: if opt.save:
self.save() self.save()
odoo.conf.addons_paths = self.options['addons_path'].split(',') odoo.conf.addons_paths = self.options['addons_path'].split(',')
if opt.server_wide_modules:
odoo.conf.server_wide_modules = map(lambda m: m.strip(), opt.server_wide_modules.split(',')) odoo.conf.server_wide_modules = [
else: m.strip() for m in server_wide_modules.split(',')
odoo.conf.server_wide_modules = ['web','web_kanban'] ]
def _is_addons_path(self, path): def _is_addons_path(self, path):
from odoo.modules.module import MANIFEST_NAMES from odoo.modules.module import MANIFEST_NAMES
+2
View File
@@ -36,9 +36,11 @@ def py2exe_datafiles():
import docutils import docutils
import passlib import passlib
import reportlab
import requests import requests
data_mapping = ((docutils, 'docutils'), data_mapping = ((docutils, 'docutils'),
(passlib, 'passlib'), (passlib, 'passlib'),
(reportlab, 'reportlab'),
(requests, 'requests')) (requests, 'requests'))
for mod, datadir in data_mapping: for mod, datadir in data_mapping:
+1 -1
View File
@@ -21,6 +21,7 @@ RUN apt-get update -qq && \
postgresql \ postgresql \
postgresql-client \ postgresql-client \
python \ python \
python-babel \
python-dateutil \ python-dateutil \
python-decorator \ python-decorator \
python-docutils \ python-docutils \
@@ -36,7 +37,6 @@ RUN apt-get update -qq && \
python-passlib \ python-passlib \
python-psutil \ python-psutil \
python-psycopg2 \ python-psycopg2 \
python-pybabel \
python-pychart \ python-pychart \
python-pydot \ python-pydot \
python-pyparsing \ python-pyparsing \