diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-10-27 18:17:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-10-27 18:17:59 +0200 |
| commit | 3bb022d57c43c51a5fd4be39e4123b1206db4782 (patch) | |
| tree | 5612b82c58085c75514800e5ca5ce96cb6bead56 | |
| parent | ba33ec755407aa3c54edbe7f668c058a83bda356 (diff) | |
| parent | 6f8e40cdecaffe70b2ed8aa91afe787c94a0a2e5 (diff) | |
[MERGE] forward port branch saas-12 up to 6f8e40c
46 files changed, 673 insertions, 211 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 45273bfbca8..e2f8e6eaeaa 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -745,7 +745,12 @@ class AccountTax(models.Model): if not round_tax: 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 # 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. for tax in self.sorted(key=lambda r: r.sequence): 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'] - base = ret['base'] + base = ret['base'] if tax.include_base_amount else base total_included = ret['total_included'] tax_amount = total_included - total_excluded taxes += ret['taxes'] diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 204f0161a85..931ae7f41df 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -472,7 +472,7 @@ class AccountBankStatementLine(models.Model): 'details': { 'name': _("Automatically reconciled items"), 'model': 'account.move', - 'ids': automatic_reconciliation_entries.ids + 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids } }] return { diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 4e8e5c9c0e4..efbbeb17011 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -612,6 +612,14 @@ class AccountInvoice(models.Model): '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), } + + # 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 @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'] for tax in taxes: 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) if key not in tax_grouped: @@ -1350,15 +1350,17 @@ class AccountInvoiceTax(models.Model): for invoice in self.mapped('invoice_id'): tax_grouped[invoice.id] = invoice.get_taxes_values() for tax in self: - key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({ - 'tax_id': tax.tax_id.id, - 'account_id': tax.account_id.id, - 'account_analytic_id': tax.account_analytic_id.id, - }) - if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]: - 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) + tax.base = 0.0 + if tax.tax_id: + key = tax.tax_id.get_grouping_key({ + 'tax_id': tax.tax_id.id, + 'account_id': tax.account_id.id, + 'account_analytic_id': tax.account_analytic_id.id, + }) + if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]: + 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) name = fields.Char(string='Tax Description', required=True) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 64a5c538841..01d3b8199f9 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -981,6 +981,9 @@ class AccountMoveLine(models.Model): return True rec_move_ids = self.env['account.partial.reconcile'] 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_credit_ids return rec_move_ids.unlink() diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 9d0b3d3da27..eb969cadec4 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -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) 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) - 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 type = fields.Selection([ ('out_invoice', 'Customer Invoice'), diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index d626f4e20e0..84aa784fad3 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -43,10 +43,46 @@ class TestTax(AccountTestUsers): (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_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 + 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): res = self.group_tax.compute_all(200.0) self.assertEquals(res['total_excluded'], 200.0) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 568bdead85d..e1da859f9d0 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -135,8 +135,8 @@ <field name="user_id"/> <field name="date_due"/> <field name="origin"/> - <field name="amount_total_signed" string="Total"/> - <field name="residual_signed" string="Amount Due"/> + <field name="amount_total_signed" string="Total" sum="Total"/> + <field name="residual_signed" string="Amount Due" sum="Amount Due"/> <field name="currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/> <field name="state"/> @@ -200,8 +200,8 @@ <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> <field name="date_due"/> <field name="origin"/> - <field name="amount_total_signed" string="Total"/> - <field name="residual_signed" string="To Pay"/> + <field name="amount_total_signed" string="Total" sum="Total"/> + <field name="residual_signed" string="To Pay" sum="To pay"/> <field name="currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/> <field name="state"/> diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 1f12802b74c..a14c4c73ffa 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -12,7 +12,7 @@ <field name="journal_id"/> <field name="payment_method_id"/> <field name="partner_id" string="Customer"/> - <field name="amount"/> + <field name="amount" sum="Amount"/> <field name="state"/> <field name="company_id" groups="base.group_multi_company"/> <field name="currency_id" invisible="1"/> @@ -31,7 +31,7 @@ <field name="journal_id"/> <field name="payment_method_id"/> <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="state"/> <field name="currency_id" invisible="1"/> @@ -149,7 +149,7 @@ </p> </field> </record> - + <record id="action_account_payments_payable" model="ir.actions.act_window"> <field name="name">Payments</field> <field name="res_model">account.payment</field> diff --git a/addons/account/views/web_planner_data.xml b/addons/account/views/web_planner_data.xml index de159a85cf0..b3569deb76b 100644 --- a/addons/account/views/web_planner_data.xml +++ b/addons/account/views/web_planner_data.xml @@ -409,7 +409,7 @@ <span class="fa fa-tasks fa-4x"></span> <h3><strong>Revenue Recognition</strong></h3> <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"> <span class="fa fa-arrow-circle-o-down"/> Install Now </a> diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index 87ff74f826a..3575f36f655 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -139,7 +139,11 @@ var DashBoard = form_common.FormWidget.extend({ }, on_close_action: function(e) { 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(); } }, @@ -223,7 +227,13 @@ var DashBoard = form_common.FormWidget.extend({ var am = new ActionManager(this), // FIXME: ideally the dashboard view shall be refactored like kanban. $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); am.appendTo($action).then(function () { am.do_action(action).then(function () { diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index fa4c6498366..2756b0ebd42 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -171,79 +171,71 @@ class HrExpense(models.Model): ''' main function that is called when trying to create the accounting entries related to an expense ''' - journal_dict = {} for expense in self: - acc_date = expense.sheet_id.accounting_date or fields.Date.context_today(self) - 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(): + journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id #create the move that will contain the accounting entries - # reference if the same for every expense - ref = len(expense_list.mapped('employee_id.address_home_id.ref')) == 1 and expense_list[0].employee_id.address_home_id.ref or False + acc_date = expense.sheet_id.accounting_date or expense.date move = self.env['account.move'].create({ 'journal_id': journal.id, 'company_id': self.env.user.company_id.id, '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 # to set it to '' which cause no number to be given to the account.move when posted. 'name': '/', }) - for expense in expense_list: - company_currency = expense.company_id.currency_id - diff_currency_p = expense.currency_id != company_currency - #one account.move.line per expense (+taxes..) - move_lines = expense._move_line_get() - - #create one more move line, a counterline for the total on payable account - payment_id = False - total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date) - if expense.payment_mode == 'company_account': - 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)) - emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id - journal = expense.bank_journal_id - #create payment - 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 - payment = self.env['account.payment'].create({ - 'payment_method_id': payment_methods and payment_methods[0].id or False, - 'payment_type': total < 0 and 'outbound' or 'inbound', - 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id, - 'partner_type': 'supplier', - 'journal_id': journal.id, - 'payment_date': expense.date, - 'state': 'reconciled', - '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), - 'name': expense.name, + company_currency = expense.company_id.currency_id + diff_currency_p = expense.currency_id != company_currency + #one account.move.line per expense (+taxes..) + move_lines = expense._move_line_get() + + #create one more move line, a counterline for the total on payable account + payment_id = False + total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date) + if expense.payment_mode == 'company_account': + 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)) + emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id + journal = expense.sheet_id.bank_journal_id + #create payment + 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 + payment = self.env['account.payment'].create({ + 'payment_method_id': payment_methods and payment_methods[0].id or False, + 'payment_type': total < 0 and 'outbound' or 'inbound', + 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id, + 'partner_type': 'supplier', + 'journal_id': journal.id, + 'payment_date': expense.date, + 'state': 'reconciled', + '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), + '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({ - '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, - }) - - #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() + + #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}) move.post() + if expense.payment_mode == 'company_account': + expense.sheet_id.paid_expense_sheets() return True @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['context'] = {'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id} 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 diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 255778731b8..5088ff3b571 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -422,6 +422,13 @@ type="object"> <field name="attachment_number" widget="statinfo" string="Documents"/> </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 class="oe_title"> <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="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="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> <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'no_delete': True}"> diff --git a/addons/l10n_in/data/account_tax_template_data.xml b/addons/l10n_in/data/account_tax_template_data.xml index 85fb8c4c6d6..f5543411b9e 100644 --- a/addons/l10n_in/data/account_tax_template_data.xml +++ b/addons/l10n_in/data/account_tax_template_data.xml @@ -605,7 +605,7 @@ <field name="account_id" ref="p112341"/> <field name="refund_account_id" ref="p20011"/> <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 id="sale_edu_cess_service_tax_2" model="account.tax.template"> <field name="name">(standard)Input Education Cess @ 2% On Service Tax</field> @@ -665,7 +665,7 @@ <field name="account_id" ref="p11232"/> <field name="refund_account_id" ref="p20011"/> <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> <!--output purchase tax--> @@ -679,7 +679,7 @@ <field name="account_id" ref="p10050"/> <field name="refund_account_id" ref="p2107"/> <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 id="output_tax_12" model="account.tax.template"> <field name="name">(standard)Output Purchase Tax @ 12%</field> @@ -691,7 +691,7 @@ <field name="account_id" ref="p10050"/> <field name="refund_account_id" ref="p2107"/> <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 id="output_tax_4" model="account.tax.template"> <field name="name">(standard)Output Purchase Tax @ 4%</field> diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 0b06d300679..8291237a2de 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -9,6 +9,7 @@ var utils = require('mail.utils'); var config = require('web.config'); var core = require('web.core'); var form_common = require('web.form_common'); +var framework = require('web.framework'); var web_utils = require('web.utils'); var _t = core._t; @@ -430,7 +431,14 @@ var Chatter = form_common.AbstractField.extend({ }, 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}); + }); }, /** diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 853bd6d56a4..39f190ece58 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -170,21 +170,31 @@ class StockMove(models.Model): ''' Creates an extra move if necessary depending on extra quantities than foreseen or extra moves''' self.ensure_one() quantity_to_split = 0 + uom_qty_to_split = 0 extra_move = self.env['stock.move'] rounding = self.product_uom.rounding 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: 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: 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: 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: - 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 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, 'procurement_id': link_procurement and self.procurement_id.id or False}) extra_move.action_confirm() @@ -203,8 +213,7 @@ class StockMove(models.Model): else: movelot.move_id = extra_move.id else: - self.quantity_done = self.product_uom_qty - self.product_uom_qty = self.quantity_done - quantity_to_split + self.quantity_done -= quantity_to_split return extra_move @api.multi diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index 7bef9923ca6..e5df59e5130 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -118,8 +118,8 @@ class StockWarehouse(models.Model): return routes @api.multi - def _handle_renaming(self, name, code): - res = super(StockWarehouse, self)._handle_renaming(name, code) + def _update_name_and_code(self, name, code): + res = super(StockWarehouse, self)._update_name_and_code(name, code) # change the manufacture procurement rule name for warehouse in self: if warehouse.manufacture_pull_id: diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ca3d71f665b..b57c4c3247c 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -214,8 +214,10 @@ class PosOrder(models.Model): partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False if move is None: # 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( - 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): # if have_to_group_by: @@ -540,7 +542,10 @@ class PosOrder(models.Model): for order in self: address = order.partner_id.address_get(['delivery']) or {} 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 if order.partner_id: destination_id = order.partner_id.property_stock_customer.id @@ -550,9 +555,10 @@ class PosOrder(models.Model): destination_id = customerloc.id else: destination_id = picking_type.default_location_dest_id.id + if picking_type: - pos_qty = all([x.qty >= 0 for x in order.lines]) - picking_id = Picking.create({ + 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_vals = { 'origin': order.name, 'partner_id': address.get('delivery', False), 'date_done': order.date_order, @@ -560,44 +566,72 @@ class PosOrder(models.Model): 'company_id': order.company_id.id, 'move_type': 'direct', 'note': order.note or "", - 'location_id': location_id if pos_qty else destination_id, - 'location_dest_id': destination_id if pos_qty else location_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) - picking_id.message_post(body=message) - order.write({'picking_id': picking_id.id}) + 'location_id': location_id, + 'location_dest_id': destination_id, + } + pos_qty = any([x.qty >= 0 for x in order.lines]) + if pos_qty: + 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']): - Move += Move.create({ + moves |= Move.create({ 'name': line.name, 'product_uom': line.product_id.uom_id.id, - 'picking_id': picking_id and picking_id.id or False, - 'picking_type_id': picking_type.id, + 'picking_id': order_picking.id if line.qty >= 0 else return_picking.id, + 'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id, 'product_id': line.product_id.id, 'product_uom_qty': abs(line.qty), 'state': 'draft', '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() - picking_id.force_assign() - order.set_pack_operation_lot() - picking_id.action_done() - elif Move: - Move.action_confirm() - Move.force_assign() - Move.action_done() + + # prefer associating the regular order picking, not the return + order.write({'picking_id': order_picking.id or return_picking.id}) + + if return_picking: + order._force_picking_done(return_picking) + if order_picking: + order._force_picking_done(order_picking) + + # 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 - 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.""" StockProductionLot = self.env['stock.production.lot'] PosPackOperationLot = self.env['pos.pack.operation.lot'] + if not picking: + picking = order.picking_id + 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_done = 0 pack_lots = [] @@ -868,15 +902,15 @@ class ReportSaleDetails(models.AbstractModel): 'payments': payments, 'company_name': self.env.user.company_id.name, 'taxes': taxes.values(), - 'products': [{ + 'products': sorted([{ 'product_id': product.id, - 'product_name': product.name[:20], + 'product_name': product.name, 'code': product.default_code, 'quantity': qty, 'price_unit': price_unit, 'discount': discount, '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 diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index ea0f8391bfe..66dfb2b6659 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -18,7 +18,9 @@ class PosSession(models.Model): for session in self: company_id = session.config_id.journal_id.company_id.id 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) for order in session.order_ids.filtered(lambda o: o.state != 'done'): if order.state not in ('paid', 'invoiced'): diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 1b044274ce8..3245c9a409b 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -871,7 +871,7 @@ <div class='orderlines' line-ratio='0.6'> <t t-foreach='products' t-as='line'> <line> - <left><t t-esc='line.product_name' /></left> + <left><t t-esc='line.product_name.substr(0,20)' /></left> <right> <value value-decimals='2' value-autoint='on'> <t t-esc='line.quantity' /> diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index e9d7bf68a7b..51e0bf5a69b 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -119,9 +119,208 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # 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.") + 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): - #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({ 'company_id': self.company_id, 'partner_id': self.partner1.id, diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 0724142d9ba..e13747f6ca1 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -392,8 +392,7 @@ class ProductProduct(models.Model): name = variant and "%s (%s)" % (product.name, variant) or product.name sellers = [] 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: sellers = [x for x in product.seller_ids if (x.name.id in partner_ids) and not x.product_id] if sellers: diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index aa2b67c4750..93b3f99a902 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -124,8 +124,8 @@ class StockWarehouse(models.Model): return routes @api.multi - def _handle_renaming(self, name, code): - res = super(StockWarehouse, self)._handle_renaming(name, code) + def _update_name_and_code(self, name, code): + res = super(StockWarehouse, self)._update_name_and_code(name, code) warehouse = self[0] #change the buy procurement rule name if warehouse.buy_pull_id: @@ -133,8 +133,8 @@ class StockWarehouse(models.Model): return res @api.multi - def change_route(self): - res = super(StockWarehouse, self).change_route() + def _update_routes(self): + res = super(StockWarehouse, self)._update_routes() for warehouse in self: 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}) diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 4059e6e02b1..9acafc84110 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -653,7 +653,7 @@ class ResourceCalendarAttendance(models.Model): 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_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): diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 81f2d7fbb30..781b986bf34 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -175,7 +175,7 @@ class SaleOrder(models.Model): return super(SaleOrder, self)._track_subtype(init_values) @api.multi - @api.onchange('partner_shipping_id') + @api.onchange('partner_shipping_id', 'partner_id') def onchange_partner_shipping_id(self): """ Trigger the change of fiscal position when the shipping address is modified. diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index b780a526c36..8dbc9f21110 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -67,9 +67,8 @@ class SaleAdvancePaymentInv(models.TransientModel): if self.product_id.id: account_id = self.product_id.property_account_income_id.id if not account_id: - prop = 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(prop_id) + inc_acc = ir_property_obj.get('property_account_income_categ_id', 'product.category') + account_id = order.fiscal_position_id.map_account(inc_acc).id if inc_acc else False if not account_id: 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.') % diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index a6ba9d5c004..bc06e4a1a12 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -10,6 +10,9 @@ from odoo import api, fields, models, registry, _ from odoo.osv import expression from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round +import logging + +_logger = logging.getLogger(__name__) class ProcurementGroup(models.Model): _inherit = 'procurement.group' @@ -172,8 +175,11 @@ class ProcurementOrder(models.Model): 'propagate': self.rule_id.propagate, '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 def _run(self): diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index f241ddd5a05..69b1f84eb79 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -6,6 +6,10 @@ from odoo.tools.translate import _ from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + class Quant(models.Model): """ 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 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 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: vals.update({'package_id': dest_package_id}) 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 def _quant_reconcile_negative(self, move): diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 900e7f18309..189618a7755 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -10,6 +10,10 @@ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +import logging + +_logger = logging.getLogger(__name__) + class Warehouse(models.Model): _name = "stock.warehouse" @@ -452,7 +456,11 @@ class Warehouse(models.Model): 'product_categ_selectable': True, 'supplied_wh_id': self.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): return { @@ -463,8 +471,10 @@ class Warehouse(models.Model): 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', 'sequence': 20} - def _get_crossdock_route(self, route_name): # compatibility - return self._get_crossdock_route_values() + def _get_crossdock_route(self, route_name): + # 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 # ------------------------------------------------------------ @@ -503,9 +513,15 @@ class Warehouse(models.Model): 'procure_method': 'make_to_order', 'active': True}, name_suffix=_('MTO')) 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}) 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'): change_to_multiple = warehouse.delivery_steps == 'ship_only' 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): """ 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_mto_pull(routes_data) 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 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.pick_type_id.sequence_id.write(sequence_data['pick_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): switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id)) diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index a350dadb59e..c160ebaf539 100644 --- a/addons/web/static/src/js/framework/data.js +++ b/addons/web/static/src/js/framework/data.js @@ -793,18 +793,19 @@ var BufferedDataSet = DataSetStatic.extend({ var def = $.Deferred(); this.mutex.exec(function () { var dirty = false; - _.each(data, function (v, k) { - if (!_.isEqual(v, cached.values[k])) { + // _.each is broken if a field "length" is present + for (var k in data) { + if (!_.isEqual(data[k], cached.values[k])) { 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]; } else { - cached.changes[k] = v; + cached.changes[k] = data[k]; } } else { delete data[k]; } - }); + } self._update_cache(id, options); if (dirty) { diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 9382b255b01..ab6cc55049d 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -470,11 +470,12 @@ var ListView = View.extend({ self.records.remove(record); 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') record.set(key + '__display', false, {silent: true}); - record.set(key, value, {silent: true}); - }); + record.set(key, values[key], {silent: true}); + } record.trigger('change', record); /* When a record is reloaded, there is a rendering lag because of the addition/suppression of diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index be2acdb1365..4b88d9f06e5 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -196,6 +196,13 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView', 'web.data_manag mock.add('demo:onchange', function () { 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) { @@ -263,6 +270,13 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView', 'web.dat mock.add('demo:read', function () { 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) { @@ -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:create', function () { return 1; }); 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 fields_view = data_manager._postprocess_fvg({ diff --git a/addons/web/static/test/list.js b/addons/web/static/test/list.js index 9d1d1b9ec7d..49adabba13d 100644 --- a/addons/web/static/test/list.js +++ b/addons/web/static/test/list.js @@ -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) { assert.expect(2); - + mock.add('demo:read', function (args, kwargs) { if (_.isEqual(args[0], [1, 2, 3])) { return [ @@ -18,7 +18,9 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manag throw new Error(JSON.stringify(_.toArray(arguments))); }); 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 fields_view = data_manager._postprocess_fvg({ diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py index c134074e789..8b138a56924 100644 --- a/addons/web/tests/test_js.py +++ b/addons/web/tests/test_js.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import unittest import odoo.tests - class WebSuite(odoo.tests.HttpCase): + @unittest.skip('Memory leak in this test lead to phantomjs crash, making it unreliable') def test_01_js(self): self.phantom_js('/web/tests?mod=web',"","", login='admin') diff --git a/addons/web_diagram/static/src/js/diagram.js b/addons/web_diagram/static/src/js/diagram.js index 4192e30a185..56664176ebe 100644 --- a/addons/web_diagram/static/src/js/diagram.js +++ b/addons/web_diagram/static/src/js/diagram.js @@ -246,16 +246,13 @@ var DiagramView = View.extend({ self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded); }); - var form_fields = [self.parent_field]; - var form_controller = pop.view_form; - - form_controller.on("load_record", self, function(){ - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var field = form_controller.fields[self.parent_field]; + if (field) { field.$input.prop('disabled', true); field.$dropdown.unbind(); - }); + } }); }, @@ -274,16 +271,13 @@ var DiagramView = View.extend({ } }).open(); - var form_controller = pop.view_form; - var form_fields = [this.parent_field]; - - form_controller.on("load_record", self, function(){ - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var field = form_controller.fields[self.parent_field]; + if (field) { field.set_value(self.id); field.dirty = true; - }); + } }); }, @@ -326,12 +320,14 @@ var DiagramView = View.extend({ } }); - var form_controller = pop.view_form; - form_controller.on("load_record", self, function(){ - form_controller.fields[self.connectors.attrs.source].set_value(node_source_id); - form_controller.fields[self.connectors.attrs.source].dirty = true; - form_controller.fields[self.connectors.attrs.destination].set_value(node_dest_id); - form_controller.fields[self.connectors.attrs.destination].dirty = true; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var source_field = form_controller.fields[self.connectors.attrs.source]; + var destination_field = form_controller.fields[self.connectors.attrs.destination]; + source_field.set_value(node_source_id); + source_field.dirty = true; + destination_field.set_value(node_dest_id); + destination_field.dirty = true; }); }, diff --git a/addons/web_planner/models/web_planner.py b/addons/web_planner/models/web_planner.py index 9f88ec4e708..3fa38c1dc7c 100644 --- a/addons/web_planner/models/web_planner.py +++ b/addons/web_planner/models/web_planner.py @@ -67,6 +67,8 @@ class Planner(models.Model): module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)], limit=1) if module: params['id'] = module.id + else: + return "#show_enterprise" return "/web#%s" % (urlencode(params),) @api.model diff --git a/addons/web_planner/static/src/js/web_planner_common.js b/addons/web_planner/static/src/js/web_planner_common.js index 93bcf2bc775..ad27739f2c5 100644 --- a/addons/web_planner/static/src/js/web_planner_common.js +++ b/addons/web_planner/static/src/js/web_planner_common.js @@ -49,6 +49,7 @@ var PlannerDialog = Widget.extend({ category_selector: 'div[menu-category-id]', events: { '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 a.btn-next': 'change_to_next_page', 'click .o_planner_close_block span': 'close_modal', @@ -420,7 +421,40 @@ var PlannerDialog = Widget.extend({ destroy: function() { this.$el.modal('hide'); 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 { diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index 92961b0ac3a..630a8919a2f 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -43,7 +43,7 @@ var ViewEditor = AceCommon.ViewEditor.extend({ var args = { key: $(document.documentElement).data('view-xmlid'), full: true, - bundles: !!$('script[src*=".assets_common"]').length + bundles: this.$('.js_include_bundles')[0].checked }; return ajax .jsonRpc('/website/customize_template_get', 'call', args) diff --git a/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js index fe6e9541ac5..5caad853fcf 100644 --- a/addons/website/static/src/js/website.backendDashboard.js +++ b/addons/website/static/src/js/website.backendDashboard.js @@ -259,7 +259,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { // Check if the user is authenticated and has the right to make API calls if (!gapi.analytics.auth.getAuthResponse()) { 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'); } else { this.make_analytics_calls($analytics_components); diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index c765e335f7c..72a5934fcd6 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -135,9 +135,9 @@ odoo.define('website.website', function (require) { var error = function (title, message, url) { return new Dialog(null, { - title: data.data ? data.data.arguments[0] : "", + title: title || "", $content: $(qweb.render('website.error_dialog', { - message: data.data ? data.data.arguments[1] : data.statusText, + message: message || "", backend_url: url, })), }).open(); diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py index 19c1cec172b..9a64504e309 100644 --- a/addons/website_mail_channel/controllers/main.py +++ b/addons/website_mail_channel/controllers/main.py @@ -15,7 +15,8 @@ class MailGroup(http.Controller): def _get_archives(self, group_id): MailMessage = request.env['mail.message'] 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") for group in groups: (r, label) = group['date'] @@ -41,7 +42,7 @@ class MailGroup(http.Controller): # compute statistics month_date = datetime.today() - relativedelta.relativedelta(months=1) 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']) message_data = dict((message['res_id'], message['res_id_count']) for message in messages) diff --git a/debian/control b/debian/control index 4a969660ccb..63d5b2247f9 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Depends: node-less, postgresql-client, python, + python-babel, python-dateutil, python-decorator, python-docutils, @@ -32,7 +33,6 @@ Depends: python-passlib, python-psutil, python-psycopg2, - python-pybabel, python-pychart, python-pydot, python-pyparsing, diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 1e23c311da2..9a105eb2415 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -131,6 +131,10 @@ class IrModel(models.Model): if model.state != 'manual': 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() res = super(IrModel, self).unlink() @@ -422,6 +426,19 @@ class IrModelFields(models.Model): 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 def unlink(self): if not self: return True @@ -431,6 +448,9 @@ class IrModelFields(models.Model): any(field.state != 'manual' for field in self): 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') self._drop_column() res = super(IrModelFields, self).unlink() @@ -522,6 +542,7 @@ class IrModelFields(models.Model): if vals.get('name', item.name) != item.name: # We need to rename the column + item._prepare_update() if column_rename: raise UserError(_('Can only rename one field at a time!')) if vals['name'] in obj._fields: diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 552878f093d..0d0d352f670 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -355,7 +355,7 @@ class HttpCase(TransactionCase): _logger.info("phantomjs: %s", line) if line == "ok": - break + return True def phantom_run(self, cmd, timeout): _logger.info('phantom_run executing %s', ' '.join(cmd)) @@ -369,8 +369,9 @@ class HttpCase(TransactionCase): phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None) except OSError: raise unittest.SkipTest("PhantomJS not found") + result = False try: - self.phantom_poll(phantom, timeout) + result = self.phantom_poll(phantom, timeout) finally: # kill phantomjs if phantom.exit() wasn't called in the test if phantom.poll() is None: @@ -379,6 +380,10 @@ class HttpCase(TransactionCase): self._wait_remaining_requests() # we ignore phantomjs return code as we kill it as soon as we have ok _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): t0 = int(time.time()) diff --git a/odoo/tools/config.py b/odoo/tools/config.py index ea030a80f10..b05fd1a018b 100644 --- a/odoo/tools/config.py +++ b/odoo/tools/config.py @@ -111,7 +111,7 @@ class configmanager(object): group.add_option("--addons-path", dest="addons_path", help="specify additional addons paths (separated by commas).", 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(), help="Directory where to store Odoo data") @@ -406,6 +406,7 @@ class configmanager(object): 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', 'data_dir', + 'server_wide_modules', ] posix_keys = [ @@ -459,14 +460,21 @@ class configmanager(object): if len(self.options['language']) > 5: 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: self.save() 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(',')) - else: - odoo.conf.server_wide_modules = ['web','web_kanban'] + + odoo.conf.server_wide_modules = [ + m.strip() for m in server_wide_modules.split(',') + ] def _is_addons_path(self, path): from odoo.modules.module import MANIFEST_NAMES @@ -36,9 +36,11 @@ def py2exe_datafiles(): import docutils import passlib + import reportlab import requests data_mapping = ((docutils, 'docutils'), (passlib, 'passlib'), + (reportlab, 'reportlab'), (requests, 'requests')) for mod, datadir in data_mapping: diff --git a/setup/package.dfdebian b/setup/package.dfdebian index 8ad98e7aba0..4c6e417dfa9 100644 --- a/setup/package.dfdebian +++ b/setup/package.dfdebian @@ -21,6 +21,7 @@ RUN apt-get update -qq && \ postgresql \ postgresql-client \ python \ + python-babel \ python-dateutil \ python-decorator \ python-docutils \ @@ -36,7 +37,6 @@ RUN apt-get update -qq && \ python-passlib \ python-psutil \ python-psycopg2 \ - python-pybabel \ python-pychart \ python-pydot \ python-pyparsing \ |
