diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 5f1ae94b93a..d565b07e54c 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -307,7 +307,10 @@ class AccountBankStatement(models.Model): #try to assign partner to bank_statement_line stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id] refs = list(set([st.name for st in st_lines_left if not stl.partner_id])) - if st_lines_left and stl_to_assign_partner and refs: + if st_lines_left and stl_to_assign_partner and refs\ + and st_lines_left[0].journal_id.default_credit_account_id\ + and st_lines_left[0].journal_id.default_debit_account_id: + sql_query = """SELECT aml.partner_id, aml.ref, stl.id FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id @@ -605,13 +608,14 @@ class AccountBankStatementLine(models.Model): acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true" select_clause = "SELECT aml.id " from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id " - where_clause = """WHERE aml.company_id = %(company_id)s - AND ( - (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s - AND aml.payment_id IS NOT NULL) - OR - ("""+acc_type+""" AND aml.reconciled = false) - )""" + account_clause = '' + if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id: + account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR" + where_clause = """WHERE aml.company_id = %(company_id)s + AND ( + """ + account_clause + """ + ("""+acc_type+""" AND aml.reconciled = false) + )""" where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause if split: diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 896e0ad5c3a..e614b944287 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -177,7 +177,7 @@ class AccountInvoice(models.Model): if payment_currency_id and payment_currency_id == self.currency_id: amount_to_show = amount_currency else: - amount_to_show = payment.company_id.currency_id.with_context(date=payment.date).compute(amount, + amount_to_show = payment.company_id.currency_id.with_context(date=self.date).compute(amount, self.currency_id) if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding): continue @@ -880,7 +880,6 @@ class AccountInvoice(models.Model): if tax.amount_type == "group": for child_tax in tax.children_tax_ids: done_taxes.append(child_tax.id) - done_taxes.append(tax.id) res.append({ 'invoice_tax_line_id': tax_line.id, 'tax_line_id': tax_line.tax_id.id, @@ -892,8 +891,9 @@ class AccountInvoice(models.Model): 'account_id': tax_line.account_id.id, 'account_analytic_id': tax_line.account_analytic_id.id, 'invoice_id': self.id, - 'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else [] + 'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else [] }) + done_taxes.append(tax.id) return res def inv_line_characteristic_hashcode(self, invoice_line): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 1daae7903a1..6b5c83c237e 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -540,6 +540,7 @@ class AccountMoveLine(models.Model): {2} WHERE a.reconcile IS TRUE + AND l.full_reconcile_id is NULL {3} {4} {5} @@ -1628,30 +1629,15 @@ class AccountPartialReconcile(models.Model): } return self.env['account.move'].create(move_vals) - @api.model - def create(self, vals): - aml = [] - if vals.get('debit_move_id', False): - aml.append(vals['debit_move_id']) - if vals.get('credit_move_id', False): - aml.append(vals['credit_move_id']) - # Get value of matched percentage from both move before reconciliating - lines = self.env['account.move.line'].browse(aml) - if lines[0].account_id.internal_type in ('receivable', 'payable'): - percentage_before_rec = lines._get_matched_percentage() - # Reconcile - res = super(AccountPartialReconcile, self).create(vals) - # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry - if lines[0].account_id.internal_type in ('receivable', 'payable'): - res.create_tax_cash_basis_entry(percentage_before_rec) + def _compute_partial_lines(self): if self._context.get('skip_full_reconcile_check'): #when running the manual reconciliation wizard, don't check the partials separately for full #reconciliation or exchange rate because it is handled manually after the whole processing - return res + return self #check if the reconcilation is full #first, gather all journal items involved in the reconciliation just created - partial_rec_set = OrderedDict.fromkeys([x for x in res]) - aml_set = self.env['account.move.line'] + partial_rec_set = OrderedDict.fromkeys([x for x in self]) + aml_set = aml_to_balance = self.env['account.move.line'] total_debit = 0 total_credit = 0 total_amount_currency = 0 @@ -1659,7 +1645,7 @@ class AccountPartialReconcile(models.Model): #possible to compute the exchange difference entry and it has to be done manually. currency = list(partial_rec_set)[0].currency_id maxdate = '0000-00-00' - aml_to_balance = self.env['account.move.line'] + for partial_rec in partial_rec_set: if partial_rec.currency_id != currency: #no exchange rate entry will be created @@ -1703,6 +1689,24 @@ class AccountPartialReconcile(models.Model): 'reconciled_line_ids': [(6, 0, aml_ids)], 'exchange_move_id': exchange_move_id, }) + + @api.model + def create(self, vals): + aml = [] + if vals.get('debit_move_id', False): + aml.append(vals['debit_move_id']) + if vals.get('credit_move_id', False): + aml.append(vals['credit_move_id']) + # Get value of matched percentage from both move before reconciliating + lines = self.env['account.move.line'].browse(aml) + if lines[0].account_id.internal_type in ('receivable', 'payable'): + percentage_before_rec = lines._get_matched_percentage() + # Reconcile + res = super(AccountPartialReconcile, self).create(vals) + # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry + if lines[0].account_id.internal_type in ('receivable', 'payable'): + res.create_tax_cash_basis_entry(percentage_before_rec) + res._compute_partial_lines() return res @api.multi diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index a0817a43bb7..f464f2069a9 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -91,7 +91,7 @@ class account_abstract_payment(models.AbstractModel): total = 0 for inv in self.invoice_ids: if inv.currency_id == payment_currency: - total += inv.residual_company_signed + total += inv.residual_signed else: total += inv.company_currency_id.with_context(date=self.payment_date).compute( inv.residual_company_signed, payment_currency) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 50012321db5..0093915eee5 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -477,3 +477,147 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(round(aml.credit, 2), line['credit']) self.assertEquals(round(aml.amount_currency, 2), line['amount_currency']) self.assertEquals(aml.currency_id.id, line['currency_id']) + + def test_partial_reconcile_currencies(self): + # client Account (payable, rsa) + # Debit Credit + # -------------------------------------------------------- + # Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100 + # Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66 + # Pay c: 25/0.8 = 31.25 | + # + # Debit_currency = 100 | Credit currency = 100 + # Debit = 147.91 | Credit = 166.66 + # Balance Debit = 18.75 + # Counterpart Credit goes in Exchange diff + + dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1) + account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1) + + self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id, + 'default_credit_account_id': self.account_rsa.id}) + dest_journal_id.write({'default_debit_account_id': self.account_rsa.id, + 'default_credit_account_id': self.account_rsa.id}) + # Setting up rates for USD (main_company is in EUR) + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01', + 'rate': 0.5, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01', + 'rate': 0.75, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01', + 'rate': 0.80, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + # Preparing Invoices (from vendor) + invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_usd_id, + 'name': 'invoice to vendor', + 'account_id': self.account_rsa.id, + 'type': 'in_invoice', + 'date_invoice': time.strftime('%Y') + '-' + '07' + '-01', + }) + self.account_invoice_line_model.create({'product_id': self.product.id, + 'quantity': 1, + 'price_unit': 50, + 'invoice_id': invoice_a.id, + 'name': 'product that cost ' + str(50), + 'account_id': account_expenses.id, + }) + + invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_usd_id, + 'name': 'invoice to vendor', + 'account_id': self.account_rsa.id, + 'type': 'in_invoice', + 'date_invoice': time.strftime('%Y') + '-' + '08' + '-01', + }) + self.account_invoice_line_model.create({'product_id': self.product.id, + 'quantity': 1, + 'price_unit': 50, + 'invoice_id': invoice_b.id, + 'name': 'product that cost ' + str(50), + 'account_id': account_expenses.id, + }) + + invoice_a.action_invoice_open() + invoice_b.action_invoice_open() + + # Preparing Payments + # One partial for invoice_a (fully assigned to it) + payment_a = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '07' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + # One that will complete the payment of a, the rest goes to b + payment_b = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 50, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '08' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + # The last one will complete the payment of b + payment_c = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '09' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + payment_a.post() + payment_b.post() + payment_c.post() + + # Assigning payments to invoices + debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + + invoice_a.assign_outstanding_credit(debit_line_a.id) + invoice_a.assign_outstanding_credit(debit_line_b.id) + invoice_b.assign_outstanding_credit(debit_line_b.id) + invoice_b.assign_outstanding_credit(debit_line_c.id) + + # Asserting correctness (only in the payable account) + full_reconcile = False + for inv in (invoice_a + invoice_b): + self.assertTrue(inv.reconciled) + for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa): + self.assertEqual(aml.amount_residual, 0.0) + self.assertEqual(aml.amount_residual_currency, 0.0) + self.assertTrue(aml.reconciled) + if not full_reconcile: + full_reconcile = aml.full_reconcile_id + else: + self.assertTrue(aml.full_reconcile_id == full_reconcile) + + full_rec_move = full_reconcile.exchange_move_id + # Globally check whether the amount is correct + self.assertEqual(full_rec_move.amount, 18.75) + + # Checking if the direction of the move is correct + full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa) + self.assertEqual(full_rec_payable.balance, 18.75) diff --git a/addons/account/views/account_config_settings_views.xml b/addons/account/views/account_config_settings_views.xml index 66ff5ab2076..fd30bb12837 100644 --- a/addons/account/views/account_config_settings_views.xml +++ b/addons/account/views/account_config_settings_views.xml @@ -16,7 +16,7 @@
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index f4cafe12319..d4b65d6b020 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -108,3 +108,6 @@ class AccountInvoiceLine(models.Model): elif invoice.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id super(AccountInvoiceLine, self)._set_additional_fields(invoice) + + def get_invoice_line_account(self, type, product, fpos, company): + return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/addons/account_asset/static/tests/account_asset_tests.js b/addons/account_asset/static/tests/account_asset_tests.js index 8f353f3bf5c..3ae5ac40073 100644 --- a/addons/account_asset/static/tests/account_asset_tests.js +++ b/addons/account_asset/static/tests/account_asset_tests.js @@ -125,7 +125,7 @@ QUnit.test('basic rendering', function (assert) { }); QUnit.test('click events are correctly triggered', function (assert) { - assert.expect(3); + assert.expect(2); var form = createView({ View: FormView, @@ -146,8 +146,7 @@ QUnit.test('click events are correctly triggered', function (assert) { intercepts: { execute_action: function (event) { var data = event.data; - assert.strictEqual(data.model, 'line', "should have correct model"); - assert.deepEqual(data.res_ids, [4], "should have correct res_ids"); + assert.strictEqual(data.env.model, 'line', "should have correct model"); assert.strictEqual(data.action_data.name, 'create_move', "should call correct method"); }, diff --git a/addons/base_setup/views/base_config_settings_views.xml b/addons/base_setup/views/base_config_settings_views.xml index 6d4f9277de1..a0dab58c570 100644 --- a/addons/base_setup/views/base_config_settings_views.xml +++ b/addons/base_setup/views/base_config_settings_views.xml @@ -7,7 +7,7 @@
diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 6fb25a5b43a..6efeeecaccd 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -13,9 +13,10 @@ class StockQuantPackage(models.Model): @api.one @api.depends('quant_ids') def _compute_weight(self): - weight = 0 - for quant in self.quant_ids: - weight += quant.qty * quant.product_id.weight + smls = self.env['stock.move.line'].search([('product_id', '!=', False), ('result_package_id', '=', self.id)]) + weight = 0.0 + for sml in smls: + weight += sml.product_uom_id._compute_quantity(sml.qty_done, sml.product_id.uom_id) * sml.product_id.weight self.weight = weight weight = fields.Float(compute='_compute_weight') @@ -60,7 +61,7 @@ class StockPicking(models.Model): self.ensure_one() packs = set() for move_line in self.move_line_ids: - if move_line.result_package_id and move_line.result_package_id.packaging_id: + if move_line.result_package_id: packs.add(move_line.result_package_id.id) self.package_ids = list(packs) @@ -70,7 +71,7 @@ class StockPicking(models.Model): weight = 0.0 for move_line in self.move_line_ids: if move_line.product_id and not move_line.result_package_id: - weight += move_line.product_uom_id._compute_quantity(move_line.product_qty, move_line.product_id.uom_id) * move_line.product_id.weight + weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight self.weight_bulk = weight @api.one diff --git a/addons/event/models/event.py b/addons/event/models/event.py index f0d6d0312cf..95310a67122 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -316,6 +316,9 @@ class EventEvent(models.Model): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) + @api.multi + def _is_event_registrable(self): + return True class EventRegistration(models.Model): _name = 'event.registration' diff --git a/addons/event/views/event_config_settings_views.xml b/addons/event/views/event_config_settings_views.xml index aba1ec2f607..5d9ea62c0f8 100644 --- a/addons/event/views/event_config_settings_views.xml +++ b/addons/event/views/event_config_settings_views.xml @@ -8,7 +8,7 @@

Advertising

diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index a0d27bd268f..d1c28b32d2a 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -53,6 +53,12 @@ class Event(models.Model): }) for ticket in self.event_type_id.event_ticket_ids] + @api.multi + def _is_event_registrable(self): + self.ensure_one() + if not self.event_ticket_ids: + return True + return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active)) class EventTicket(models.Model): _name = 'event.event.ticket' diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml index 3b983ebd256..de76683ddc0 100644 --- a/addons/gamification/data/goal_base.xml +++ b/addons/gamification/data/goal_base.xml @@ -98,7 +98,7 @@ % else: - The following message contains the current progress for the challenge ${object.name}. + Challenge: ${object.name}. % for line in challenge_lines: @@ -113,7 +113,7 @@ % if len(line['goals'])>2:
- +
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]] % for goal in top_goals: @@ -144,9 +144,10 @@
${extra_div | safe}
- + % set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small + ${goal['name']}
-
+
${goal['name']}
@@ -177,8 +178,8 @@
- - + + - - + - + diff --git a/addons/hr_attendance/views/attendance_config_settings_views.xml b/addons/hr_attendance/views/attendance_config_settings_views.xml index b8c11252fbf..cf72f9f2220 100644 --- a/addons/hr_attendance/views/attendance_config_settings_views.xml +++ b/addons/hr_attendance/views/attendance_config_settings_views.xml @@ -7,7 +7,7 @@

Check-In/Out

@@ -64,4 +64,4 @@ - \ No newline at end of file + diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index cdcf25b491f..ce0054a5636 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -385,7 +385,7 @@ class HrExpenseSheet(models.Model): _order = "accounting_date desc, id desc" name = fields.Char(string='Expense Report Summary', required=True) - expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False) + expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False) state = fields.Selection([('submit', 'Submitted'), ('approve', 'Approved'), ('post', 'Posted'), diff --git a/addons/hr_expense/views/hr_expense_config_settings_views.xml b/addons/hr_expense/views/hr_expense_config_settings_views.xml index 19078d6071c..897e78837ca 100644 --- a/addons/hr_expense/views/hr_expense_config_settings_views.xml +++ b/addons/hr_expense/views/hr_expense_config_settings_views.xml @@ -9,7 +9,7 @@

Recording

diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 26a58ea79ce..643e62b5505 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -392,7 +392,7 @@ - + diff --git a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml index 548ccddcc28..673e35c1992 100644 --- a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml +++ b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml @@ -15,7 +15,7 @@
diff --git a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml index 54499005565..6617b8d4153 100644 --- a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml @@ -8,7 +8,7 @@

Job Posting

diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index d8d07c42fd8..3023fdcea59 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model): @api.model def default_get(self, field_list): result = super(AccountAnalyticLine, self).default_get(field_list) - if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'): + if 'employee_id' in field_list and result.get('user_id'): result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id return result diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 988fed44727..e9bf605c382 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -571,6 +571,7 @@ class MailTemplate(models.Model): 'name': attachment[0], 'datas_fname': attachment[0], 'datas': attachment[1], + 'type': 'binary', 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 044f1f66d26..60c40a69aa9 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -950,13 +950,14 @@ class MailThread(models.AbstractModel): # Alias: check alias_contact settings if alias: + obj = None if thread_id: obj = record_set[0] - elif alias.alias_parent_thread_id: + elif alias.alias_parent_model_id and alias.alias_parent_thread_id: obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id) - elif model and hasattr(record_set, '_alias_check_contact'): + elif model: obj = self.env[model] - else: + if not hasattr(obj, '_alias_check_contact'): obj = self.env['mail.alias.mixin'] check_result = obj._alias_check_contact(message, message_dict, alias) if check_result is not True: @@ -1727,6 +1728,7 @@ class MailThread(models.AbstractModel): data_attach = { 'name': name, 'datas': base64.b64encode(str(content)), + 'type': 'binary', 'datas_fname': name, 'description': name, 'res_model': message_data['model'], diff --git a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml index d7164d017d7..7c9dd4e0382 100644 --- a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml +++ b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml @@ -7,7 +7,7 @@

Design

diff --git a/addons/mrp/models/__init__.py b/addons/mrp/models/__init__.py index 8f7da9ed59f..7b3b3958084 100644 --- a/addons/mrp/models/__init__.py +++ b/addons/mrp/models/__init__.py @@ -8,6 +8,7 @@ from . import mrp_message from . import mrp_routing from . import mrp_workcenter from . import mrp_production +from . import stock_traceability from . import mrp_unbuild from . import mrp_workorder from . import procurement diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 09841db809c..142e55d7ba5 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -89,36 +89,48 @@ class MrpUnbuild(models.Model): consume_move = self._generate_consume_moves()[0] produce_moves = self._generate_produce_moves() + + if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves): + raise UserError(_('You should specify a manufacturing order in order to find the correct tracked products.')) + if consume_move.has_tracking != 'none': self.env['stock.move.line'].create({ 'move_id': consume_move.id, 'lot_id': self.lot_id.id, 'qty_done': consume_move.product_uom_qty, - 'product_qty': consume_move.product_uom_qty, + 'product_id': consume_move.product_id.id, + 'product_uom_id': consume_move.product_uom.id, 'location_id': consume_move.location_id.id, - 'location_dest_id': consume_move.location_dest_id.id,}) + 'location_dest_id': consume_move.location_dest_id.id, + }) else: consume_move.quantity_done = consume_move.product_uom_qty consume_move.action_done() - original_quants = consume_move.quant_ids.mapped('consumed_quant_ids') - #TODO: needs to be replaced by checking the different stock.move.lots + # TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took for produce_move in produce_moves: if produce_move.has_tracking != 'none': - original = original_quants.filtered(lambda quant: quant.product_id == produce_move.product_id) - self.env['stock.move.line'].create({ - 'move_id': produce_move.id, - 'lot_id': original.lot_id.id, - 'qty_done': produce_move.product_uom_qty, - 'product_qty': produce_move.product_uom_qty, - 'location_id': produce_move.location_id.id, - 'location_dest_id': produce_move.location_dest_id.id, - }) + original_move = self.mo_id.move_raw_ids.filtered(lambda move: move.product_id == produce_move.product_id) + needed_quantity = produce_move.product_qty + for move_lines in original_move.mapped('move_line_ids'): + # Iterate over all move_lines until we unbuilded the correct quantity. + taken_quantity = min(needed_quantity, move_lines.qty_done) + if taken_quantity: + self.env['stock.move.line'].create({ + 'move_id': produce_move.id, + 'lot_id': move_lines.lot_id.id, + 'qty_done': taken_quantity, + 'product_id': produce_move.product_id.id, + 'product_uom_id': move_lines.product_uom_id.id, + 'location_id': produce_move.location_id.id, + 'location_dest_id': produce_move.location_dest_id.id, + }) + needed_quantity -= taken_quantity else: produce_move.quantity_done = produce_move.product_uom_qty produce_moves.action_done() - produced_quant_ids = produce_moves.mapped('quant_ids').filtered(lambda quant: quant.qty > 0) - consume_move.quant_ids.sudo().write({'produced_quant_ids': [(6, 0, produced_quant_ids.ids)]}) + produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0) + consume_move.move_line_ids.write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]}) return self.write({'state': 'done'}) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index f9b420578b7..5493e5928bc 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -203,7 +203,8 @@ class MrpWorkorder(models.Model): 'move_id': move.id, 'product_id': move.product_id.id, 'lot_id': False, - 'product_qty': 0.0, + 'product_uom_qty': 0.0, + 'product_uom_id': move.product_uom.id, 'qty_done': min(1.0, qty_todo), 'workorder_id': self.id, 'done_wo': False, diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 0cb7f568368..43ef948a626 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -131,8 +131,8 @@ class StockMove(models.Model): @api.multi def action_confirm(self): - moves = self.env['stock.move'] - for move in self: + moves = self + for move in self.filtered(lambda m: m.production_id): moves |= move.action_explode() # we go further with the list of ids potentially changed by action_explode return super(StockMove, moves).action_confirm() diff --git a/addons/mrp/models/stock_quant.py b/addons/mrp/models/stock_quant.py deleted file mode 100644 index 6074735a737..00000000000 --- a/addons/mrp/models/stock_quant.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -class StockQuant(models.Model): - _inherit = 'stock.quant' - - consumed_quant_ids = fields.Many2many( - 'stock.quant', 'stock_quant_consume_rel', 'produce_quant_id', 'consume_quant_id') - produced_quant_ids = fields.Many2many( - 'stock.quant', 'stock_quant_consume_rel', 'consume_quant_id', 'produce_quant_id') - - def _prepare_history(self): - vals = super(StockQuant, self)._prepare_history() - vals['consumed_quant_ids'] = [(4, quant.id) for quant in self.consumed_quant_ids] - vals['produced_quant_ids'] = [(4, quant.id) for quant in self.produced_quant_ids] - return vals diff --git a/addons/mrp/models/stock_traceability.py b/addons/mrp/models/stock_traceability.py new file mode 100644 index 00000000000..2d5c0aae6b7 --- /dev/null +++ b/addons/mrp/models/stock_traceability.py @@ -0,0 +1,21 @@ +from odoo import models, api + +class MrpStockReport(models.TransientModel): + _inherit = 'stock.traceability.report' + + @api.model + def get_links(self, move_line): + res_model, res_id, ref = super(MrpStockReport, self).get_links(move_line) + if move_line.production_id: + res_model = 'mrp.production' + res_id = move_line.production_id.id + ref = move_line.production_id.name + if move_line.move_id.unbuild_id: + res_model = 'mrp.unbuild' + res_id = move_line.move_id.unbuild_id.id + ref = move_line.move_id.unbuild_id.name + if move_line.move_id.consume_unbuild_id: + res_model = 'mrp.unbuild' + res_id = move_line.move_id.consume_unbuild_id.id + ref = move_line.move_id.consume_unbuild_id.name + return res_model, res_id, ref diff --git a/addons/mrp/report/mrp_bom_cost_report.py b/addons/mrp/report/mrp_bom_cost_report.py index d5056fe8945..d71be4d1a7e 100644 --- a/addons/mrp/report/mrp_bom_cost_report.py +++ b/addons/mrp/report/mrp_bom_cost_report.py @@ -5,7 +5,7 @@ from odoo import api, models class MrpBomCost(models.AbstractModel): - _name = 'report.mrp_bom_cost' + _name = 'report.mrp.mrp_bom_cost_report' @api.multi def get_lines(self, boms): @@ -44,4 +44,4 @@ class MrpBomCost(models.AbstractModel): def get_report_values(self, docids, data=None): boms = self.env['mrp.bom'].browse(docids) res = self.get_lines(boms) - return self.env['ir.actions.report'].render_template('mrp.mrp_bom_cost_report', {'lines': res}) + return {'lines': res} diff --git a/addons/mrp/report/mrp_bom_structure_report.py b/addons/mrp/report/mrp_bom_structure_report.py index 877db7b9570..a98f45b4754 100644 --- a/addons/mrp/report/mrp_bom_structure_report.py +++ b/addons/mrp/report/mrp_bom_structure_report.py @@ -5,7 +5,7 @@ from odoo import api, models class BomStructureReport(models.AbstractModel): - _name = 'report.mrp.report_mrpbomstructure' + _name = 'report.mrp.mrp_bom_structure_report' def get_children(self, object, level=0): result = [] diff --git a/addons/mrp/report/mrp_report_views_main.xml b/addons/mrp/report/mrp_report_views_main.xml index a54a5209a1f..064558ddad1 100644 --- a/addons/mrp/report/mrp_report_views_main.xml +++ b/addons/mrp/report/mrp_report_views_main.xml @@ -5,7 +5,7 @@ string="BOM Structure" id="action_report_bom_structure" model="mrp.bom" - name="mrp.report_mrpbomstructure" + name="mrp.mrp_bom_structure_report" file="mrp.report.mrp_bom_templates" report_type="qweb-pdf" print_report_name="'BOM - %s' % (object.product_id.name or object.product_tmpl_id.name or '')" @@ -25,8 +25,8 @@ string="BOM Cost" id="action_report_bom_price" model="mrp.bom" - name="mrp_bom_cost" - file="mrp_bom_cost" + name="mrp.mrp_bom_cost_report" + file="mrp.mrp_bom_cost_report" report_type="qweb-html" print_report_name="'BOM Cost - %s' % (object.product_id.name or object.product_tmpl_id.name or '')" /> diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index 309b7a797f4..c07593f0cae 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -4,4 +4,5 @@ from . import test_bom from . import test_order from . import test_stock from . import test_workorder_operation -from . import test_procurement \ No newline at end of file +from . import test_procurement +from . import test_unbuild \ No newline at end of file diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py new file mode 100644 index 00000000000..1a184895c93 --- /dev/null +++ b/addons/mrp/tests/test_unbuild.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mrp.tests.common import TestMrpCommon +from odoo.exceptions import UserError + + +class TestUnbuild(TestMrpCommon): + def setUp(self): + super(TestUnbuild, self).setUp() + self.stock_location = self.env.ref('stock.stock_location_stock') + + def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none'): + """ This function generate a manufacturing order with one final product + and two consumed product. Arguments allows to choose the tracking for each + different products. + It returns the MO, used bom, the tree products + """ + product_to_build = self.env['product.product'].create({ + 'name': 'Young Tom', + 'type': 'product', + 'tracking': tracking_final, + }) + product_to_use_1 = self.env['product.product'].create({ + 'name': 'Botox', + 'type': 'product', + 'tracking': tracking_base_1, + }) + product_to_use_2 = self.env['product.product'].create({ + 'name': 'Old Tom', + 'type': 'product', + 'tracking': tracking_base_2, + }) + bom_1 = self.env['mrp.bom'].create({ + 'product_id': product_to_build.id, + 'product_tmpl_id': product_to_build.product_tmpl_id.id, + 'product_uom_id': self.uom_unit.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': product_to_use_2.id, 'product_qty': 1}), + (0, 0, {'product_id': product_to_use_1.id, 'product_qty': 4}) + ]}) + mo = self.env['mrp.production'].create({ + 'name': 'MO 1', + 'product_id': product_to_build.id, + 'product_uom_id': product_to_build.uom_id.id, + 'product_qty': 5.0, + 'bom_id': bom_1.id, + }) + return mo, bom_1, product_to_build, product_to_use_1, product_to_use_2 + + def test_unbuild_standart(self): + """ This test creates a MO and then creates 3 unbuild + orders for the final product. None of the products for this + test are tracked. It checks the stock state after each order + and ensure it is correct. + """ + mo, bom, p_final, p1, p2 = self.generate_mo() + self.assertEqual(len(mo), 1, 'MO should have been created') + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + mo.action_assign() + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + }) + produce_wizard.do_produce() + + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + + # Check quantity in stock before unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock') + + # --------------------------------------------------- + # unbuild + # --------------------------------------------------- + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 5.0, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + # Check quantity in stock after last unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock') + + def test_unbuild_with_final_lot(self): + """ This test creates a MO and then creates 3 unbuild + orders for the final product. Only the final product is tracked + by lot. It checks the stock state after each order + and ensure it is correct. + """ + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot') + self.assertEqual(len(mo), 1, 'MO should have been created') + + lot = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': p_final.id, + }) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + mo.action_assign() + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + 'lot_id': lot.id, + }) + produce_wizard.do_produce() + + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + + # Check quantity in stock before unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 5, 'You should have the 5 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock') + + # --------------------------------------------------- + # unbuild + # --------------------------------------------------- + + unbuild_order = self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_unit.id, + }) + + # This should fail since we do not choose a lot to unbuild for final product. + with self.assertRaises(UserError): + unbuild_order.action_unbuild() + + unbuild_order.lot_id = lot.id + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 2, 'You should have consumed 3 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 2.0, + 'lot_id': lot.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 0, 'You should have 0 finalproduct in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 5.0, + 'lot_id': lot.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), -5, 'You should have negative quantity for final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock') + + def test_unbuild_with_comnsumed_lot(self): + """ This test creates a MO and then creates 3 unbuild + orders for the final product. Only once of the two consumed + product is tracked by lot. It checks the stock state after each + order and ensure it is correct. + """ + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot') + self.assertEqual(len(mo), 1, 'MO should have been created') + + lot = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': p1.id, + }) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + mo.action_assign() + for ml in mo.move_raw_ids.mapped('move_line_ids'): + ml.qty_done = ml.product_qty + if ml.product_id.tracking != 'none': + self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.') + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + }) + produce_wizard.do_produce() + + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + # Check quantity in stock before unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 80, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock') + + # --------------------------------------------------- + # unbuild + # --------------------------------------------------- + + unbuild_order = self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_unit.id, + }) + + # This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore) + with self.assertRaises(UserError): + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock') + + unbuild_order.mo_id = mo.id + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 92, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 2.0, + 'mo_id': mo.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 100, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 5.0, + 'mo_id': mo.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 120, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock') + + def test_unbuild_with_everything_tracked(self): + """ This test creates a MO and then creates 3 unbuild + orders for the final product. All the products for this + test are tracked. It checks the stock state after each order + and ensure it is correct. + """ + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_2='lot', tracking_base_1='lot') + self.assertEqual(len(mo), 1, 'MO should have been created') + + lot_final = self.env['stock.production.lot'].create({ + 'name': 'lot_final', + 'product_id': p_final.id, + }) + lot_1 = self.env['stock.production.lot'].create({ + 'name': 'lot_consumed_1', + 'product_id': p1.id, + }) + lot_2 = self.env['stock.production.lot'].create({ + 'name': 'lot_consumed_2', + 'product_id': p2.id, + }) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2) + mo.action_assign() + for ml in mo.move_raw_ids.mapped('move_line_ids'): + ml.qty_done = ml.product_qty + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + 'lot_id': lot_final.id, + }) + produce_wizard.do_produce() + + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + # Check quantity in stock before unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have the 5 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 80, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 5 product in stock') + + # --------------------------------------------------- + # unbuild + # --------------------------------------------------- + + unbuild_order = self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_unit.id, + }) + + with self.assertRaises(UserError): + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock') + + unbuild_order.mo_id = mo.id + with self.assertRaises(UserError): + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock') + + unbuild_order.lot_id = lot_final.id + unbuild_order.action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 2.0, + 'mo_id': mo.id, + 'lot_id': lot_final.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 0, 'You should have 0 finalproduct in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 100, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 5, 'You should have consumed all the 5 product in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 5.0, + 'mo_id': mo.id, + 'lot_id': lot_final.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), -5, 'You should have negative quantity for final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock') + + def test_unbuild_with_duplicate_move(self): + """ This test creates a MO from 3 different lot on a consumed product (p2). + The unbuild order should revert the correct quantity for each specific lot. + """ + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='none', tracking_base_2='lot', tracking_base_1='none') + self.assertEqual(len(mo), 1, 'MO should have been created') + + lot_1 = self.env['stock.production.lot'].create({ + 'name': 'lot_1', + 'product_id': p2.id, + }) + lot_2 = self.env['stock.production.lot'].create({ + 'name': 'lot_2', + 'product_id': p2.id, + }) + lot_3 = self.env['stock.production.lot'].create({ + 'name': 'lot_3', + 'product_id': p2.id, + }) + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_2) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3) + mo.action_assign() + for ml in mo.move_raw_ids.mapped('move_line_ids'): + ml.qty_done = ml.product_qty + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + }) + produce_wizard.do_produce() + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + # Check quantity in stock before unbuild. + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 0, 'You should have consumed all the 1 product for lot 1 in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 3 product for lot 2 in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 1, 'You should have consumed only 1 product for lot3 in stock') + + self.env['mrp.unbuild'].create({ + 'product_id': p_final.id, + 'bom_id': bom.id, + 'product_qty': 5.0, + 'mo_id': mo.id, + 'product_uom_id': self.uom_unit.id, + }).action_unbuild() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have no more final product in stock after unbuild') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3') diff --git a/addons/mrp/views/mrp_config_settings_views.xml b/addons/mrp/views/mrp_config_settings_views.xml index df407343714..5b2ca1593f8 100644 --- a/addons/mrp/views/mrp_config_settings_views.xml +++ b/addons/mrp/views/mrp_config_settings_views.xml @@ -8,7 +8,7 @@

Operations

diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml index ebd6f1f3c6c..6dc5a9729d1 100644 --- a/addons/mrp/views/stock_move_views.xml +++ b/addons/mrp/views/stock_move_views.xml @@ -26,14 +26,20 @@ + + - + + + + + diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index d0e4837c1d5..b887ac2a62d 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -82,7 +82,7 @@ class MrpProductProduce(models.TransientModel): quantity = self.product_qty if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_('You should at least produce some quantity')) - for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')): + for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.quantity_done == 0 and x.state not in ('done', 'cancel')): if move.unit_factor: rounding = move.product_uom.rounding move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding) diff --git a/addons/mrp_repair/models/__init__.py b/addons/mrp_repair/models/__init__.py index ee2e2755f80..99c5bc6cb6f 100644 --- a/addons/mrp_repair/models/__init__.py +++ b/addons/mrp_repair/models/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import mrp_repair +from . import stock_traceability diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index 0fab1440d12..e61f72abccf 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -12,14 +12,6 @@ class StockMove(models.Model): _inherit = 'stock.move' repair_id = fields.Many2one('mrp.repair') - consume_repair_id = fields.Many2one('mrp.repair') - - -class StockMoveLine(models.Model): - _inherit = 'stock.move.line' - - repair_id = fields.Many2one('mrp.repair', related='move_id.repair_id') - consume_repair_id = fields.Many2one('mrp.repair', related='move_id.consume_repair_id') class Repair(models.Model): @@ -442,7 +434,7 @@ class Repair(models.Model): 'result_package_id': False, 'location_id': operation.location_id.id, #TODO: owner stuff 'location_dest_id': operation.location_dest_id.id,})], - 'consume_repair_id': repair.id, + 'repair_id': repair.id, }) moves |= move operation.write({'move_id': move.id, 'state': 'done'}) diff --git a/addons/mrp_repair/models/stock_traceability.py b/addons/mrp_repair/models/stock_traceability.py new file mode 100644 index 00000000000..9d083118548 --- /dev/null +++ b/addons/mrp_repair/models/stock_traceability.py @@ -0,0 +1,13 @@ +from odoo import models, api + +class MrpStockReport(models.TransientModel): + _inherit = 'stock.traceability.report' + + @api.model + def get_links(self, move_line): + res_model, res_id, ref = super(MrpStockReport, self).get_links(move_line) + if move_line.move_id.repair_id: + res_model = 'mrp.repair' + res_id = move_line.move_id.repair_id.id + ref = move_line.move_id.repair_id.name + return res_model, res_id, ref diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py index aab5abe3244..945e24f4308 100644 --- a/addons/pad/models/pad.py +++ b/addons/pad/models/pad.py @@ -122,7 +122,7 @@ class PadCommon(models.AbstractModel): # Update the pad if the `pad_content_field` is modified for k, field in pycompat.items(self._fields): - if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field): + if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]: company = self.env.user.sudo().company_id myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api') path = self[k].split('/p/')[1] diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 4068ff24ead..1d162bdbc87 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -55,7 +55,7 @@ var FieldPad = AbstractField.extend({ // Guess what we decided... var url = {}; url.toJSON = _.constant(this.url); - this._setValue(url); + this._setValue(url, {doNotSetDirty: true}); } return this._super.apply(this, arguments); diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js index 5d18e55c1e0..dedf799db66 100644 --- a/addons/pad/static/tests/pad_tests.js +++ b/addons/pad/static/tests/pad_tests.js @@ -193,5 +193,41 @@ QUnit.module('pad widget', { delete FieldPad.prototype.isPadConfigured; }); + QUnit.test('pad widget is not considered dirty at edition', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'task', + data: this.data, + arch:'' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 2, + mockRPC: function (route, args) { + if (!args.method) { + return $.when(true); + } + return this._super.apply(this, arguments); + }, + session: { + userName: "batman", + }, + }); + form.$buttons.find('.o_form_button_edit').click(); + var def = form.canBeDiscarded(); + + assert.strictEqual($('.modal').length, 0, + "should have no confirmation modal opened"); + + assert.strictEqual(def.state(), 'resolved', + "can be discarded was succesfully resolved"); + form.destroy(); + delete FieldPad.prototype.isPadConfigured; + }); }); diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index 9455f4e1491..e43e0cbd71f 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__) # changelog https://stripe.com/docs/upgrades#api-changelog STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'} +# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal +INT_CURRENCIES = [ + 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV', + 'VND', 'XOF' +]; + class PaymentAcquirerStripe(models.Model): _inherit = 'payment.acquirer' @@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model): def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None): api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url()) charge_params = { - 'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support) + 'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100), 'currency': self.currency_id.name, 'metadata[reference]': self.reference } diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index 159bdc14214..3a736330657 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -1,6 +1,14 @@ odoo.define('payment_stripe.stripe', function(require) { "use strict"; + var ajax = require('web.ajax'); + // The following currencies are integer only, see + // https://stripe.com/docs/currencies#zero-decimal + var int_currencies = [ + 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', + 'RWF', 'KRW', 'VUV', 'VND', 'XOF' + ]; + var handler = StripeCheckout.configure({ key: $("input[name='stripe_key']").val(), image: $("input[name='stripe_image']").val(), @@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) { e.preventDefault(); if ($('.o_website_payment').length !== 0) { + var currency = $("input[name='currency']").val(); + var amount = parseFloat($("input[name='amount']").val() || '0.0'); + if (!_.contains(int_currencies, currency)) { + amount = amount*100; + } + ajax.jsonRpc('/website_payment/transaction', 'call', { reference: $("input[name='invoice_num']").val(), - amount: $("input[name='amount']").val(), - currency_id: $("input[name='currency_id']").val(), + amount: amount, + currency_id: currency, acquirer_id: acquirer_id }) handler.open({ name: $("input[name='merchant']").val(), description: $("input[name='invoice_num']").val(), - currency: $("input[name='currency']").val(), - amount: $("input[name='amount']").val()*100 + currency: currency, + amount: amount, }); } else { + var currency = $("input[name='currency']").val(); + var amount = parseFloat($("input[name='amount']").val() || '0.0'); + if (!_.contains(int_currencies, currency)) { + amount = amount*100; + } + ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', { so_id: so_id, access_token: access_token @@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) { handler.open({ name: $("input[name='merchant']").val(), description: $("input[name='invoice_num']").val(), - currency: $("input[name='currency']").val(), - amount: $("input[name='amount']").val()*100 + currency: currency, + amount: amount, }); }); } diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index f848a64cbe1..5e059a41cfc 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -870,7 +870,7 @@ class PosOrderLine(models.Model): def create(self, values): if values.get('order_id') and not values.get('name'): # set name based on the sequence specified on the config - config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id + config_id = self.order_id.browse(values['order_id']).session_id.config_id.id # HACK: sequence created in the same transaction as the config # cf TODO master is pos.config create # remove me saas-15 diff --git a/addons/point_of_sale/report/pos_invoice.py b/addons/point_of_sale/report/pos_invoice.py index 55482125444..7e25d2c6a80 100644 --- a/addons/point_of_sale/report/pos_invoice.py +++ b/addons/point_of_sale/report/pos_invoice.py @@ -23,4 +23,4 @@ class PosInvoiceReport(models.AbstractModel): not_invoiced_orders_names = [a.name for a in not_invoiced_posorders] raise UserError(_('No link to an invoice for %s.') % ', '.join(not_invoiced_orders_names)) - return self.env['ir.actions.report'].sudo().render_template('account.report_invoice', {'docs': self.env['account.invoice'].sudo().browse(ids_to_print)}) + return {'docs': self.env['account.invoice'].sudo().browse(ids_to_print)} 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 ac9831e343d..b621d5ba16f 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 @@ -397,12 +397,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.') # I generate an invoice from the order - self.invoice = self.pos_order_pos1.action_pos_order_invoice() + res = self.pos_order_pos1.action_pos_order_invoice() + self.assertIn('res_id', res, "No invoice created") # I test that the total of the attached invoice is correct - self.amount_total = self.pos_order_pos1.amount_total + invoice = self.env['account.invoice'].browse(res['res_id']) self.assertEqual( - float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") + float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports""" diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 19826462116..a18fb920306 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -25,4 +25,6 @@ point_of_sale.report_saledetails +
RankNameRankName Performance % if line['suffix']: (${line['suffix']}) @@ -199,12 +200,12 @@ % set tdColor = 'gray' % set mutedColor = '#AAAAAA' % set tdPercentageColor = '#9A6C8E' - ${goal['rank']+1} + ${goal['rank']+1} ${goal['name']}${"%.2f" % goal['current']}
on ${"%.2f" % line['target']} +
${goal['name']}${"%.2f" % goal['current']}
on ${"%.2f" % line['target']}
${goal['completeness'] | int}%${goal['completeness'] | int}%