diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 291be1aa22e..fea4e941783 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -233,14 +233,19 @@ class AccountBankStatement(models.Model): statements = self.filtered(lambda r: r.state == 'open') for statement in statements: moves = self.env['account.move'] + # `line.journal_entry_ids` gets invalidated from the cache during the loop + # because new move lines are being created at each iteration. + # The below dict is to prevent the ORM to permanently refetch `line.journal_entry_ids` + line_journal_entries = {line: line.journal_entry_ids for line in statement.line_ids} for st_line in statement.line_ids: #upon bank statement confirmation, look if some lines have the account_id set. It would trigger a journal entry #creation towards that account, with the wanted side-effect to skip that line in the bank reconciliation widget. + journal_entries = line_journal_entries[st_line] + if st_line.account_id and not journal_entries.ids: st_line.fast_counterpart_creation() - if not st_line.account_id and not st_line.journal_entry_ids.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): + if not st_line.account_id and not journal_entries.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): raise UserError(_('All the account entries lines must be processed in order to close the statement.')) - for aml in st_line.journal_entry_ids: - moves |= aml.move_id + moves = statement.mapped('line_ids.journal_entry_ids.move_id') if moves: moves.filtered(lambda m: m.state != 'posted').post() statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 2c29947ec6c..2908bf76b45 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1576,12 +1576,13 @@ class AccountPartialReconcile(models.Model): # Get value of matched percentage from both move before reconciliating lines = self.env['account.move.line'].browse(aml) lines._payment_invoice_match() - if lines[0].account_id.internal_type in ('receivable', 'payable'): + tax_cash_basis_entry = not self.env.context.get('skip_tax_cash_basis_entry') and lines[0].account_id.internal_type in ('receivable', 'payable') + if tax_cash_basis_entry: 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'): + if tax_cash_basis_entry: res.create_tax_cash_basis_entry(percentage_before_rec) res._compute_partial_lines() return res diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 15864a60d3c..1b45806e070 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -552,7 +552,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { type: 'float', name: 'amount', }], { - account_id: {string: _t("Account")}, + account_id: { + string: _t("Account"), + domain: [['deprecated', '=', false]], + }, label: {string: _t("Label")}, amount: {string: _t("Account")} }).then(function (recordID) { diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 9ff1854c3af..ca29592a051 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -54,6 +54,7 @@ var db = { code: {string: "code", type: 'integer'}, name: {string: "Displayed name", type: 'char'}, company_id: {string: "Company", type: 'many2one', relation: 'res.company'}, + deprecated: {string: "Deprecated", type: 'boolean'}, }, records: [ {id: 282, code: 100000, name: "100000 Fixed Asset Account", company_id: 1}, diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py index 3594e1cb61b..01e2c36baeb 100644 --- a/addons/base_geolocalize/models/res_partner.py +++ b/addons/base_geolocalize/models/res_partner.py @@ -30,7 +30,13 @@ def geo_find(addr, apikey=False): if result['status'] != 'OK': if result.get('error_message'): _logger.error(result['error_message']) - return None + error_msg = _('Unable to geolocate, received the error:\n%s' + '\n\nGoogle made this a paid feature.\n' + 'You should first enable billing on your Google account.\n' + 'Then, go to Developer Console, and enable the APIs:\n' + 'Geocoding, Maps Static, Maps Javascript.\n' + % result['error_message']) + raise UserError(error_msg) try: geo = result['results'][0]['geometry']['location'] diff --git a/addons/l10n_jp/data/account.tax.template.csv b/addons/l10n_jp/data/account.tax.template.csv index 1b8e083b42d..a9ef3e9ae8e 100644 --- a/addons/l10n_jp/data/account.tax.template.csv +++ b/addons/l10n_jp/data/account.tax.template.csv @@ -1,6 +1,6 @@ id,name,description,chart_template_id/id,type_tax_use,amount_type,amount,price_include,sequence,include_base_amount,account_id/id,refund_account_id/id,tag_ids/id,tax_group_id/id -tax_in_e,仮受消費税(外),仮受消費税(外),l10n_jp1,sale,percent,8,FALSE,1,FALSE,A21807,A21807,"tag_jp_03,tag_jp_12",tax_group_8 -tax_in_i,仮受消費税(内),仮受消費税(内),l10n_jp1,sale,percent,8,TRUE,1,FALSE,A21807,A21807,"tag_jp_03,tag_jp_12",tax_group_8 +tax_in_e,仮受消費税(外),仮受消費税(外),l10n_jp1,sale,percent,8,FALSE,1,FALSE,A21809,A21809,"tag_jp_03,tag_jp_12",tax_group_8 +tax_in_i,仮受消費税(内),仮受消費税(内),l10n_jp1,sale,percent,8,TRUE,1,FALSE,A21809,A21809,"tag_jp_03,tag_jp_12",tax_group_8 tax_in_x,輸出免税,輸出免税,l10n_jp1,sale,percent,,FALSE,1,FALSE,,,"tag_jp_04,tag_jp_13",tax_group_0 tax_in_o,非課税販売,非課税販売,l10n_jp1,sale,percent,,FALSE,1,FALSE,,,"tag_jp_09,tag_jp_14",tax_group_0 tax_out_e,仮払消費税(外),仮払消費税(外),l10n_jp1,purchase,percent,8,FALSE,1,FALSE,A11807,A11807,"tag_jp_07,tag_jp_16",tax_group_8 diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index 01ba23e0d4b..c2c5183bb8c 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -34,6 +34,10 @@ DIOT: 8% taxes + + DIOT: 8% NO ACREDITABLE + taxes + DIOT: Retención taxes diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 9aeefd15c06..a30f33228f7 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -483,6 +483,7 @@ class PosOrder(models.Model): return - price_unit def _reconcile_payments(self): + cash_basis_percentage_before_rec = {move: move.line_ids._get_matched_percentage() for move in self.mapped('account_move')} for order in self: aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) @@ -491,7 +492,7 @@ class PosOrder(models.Model): # Cash returns will be well reconciled # Whereas freight returns won't be # "c'est la vie..." - aml.reconcile() + aml.with_context(skip_tax_cash_basis_entry=True).reconcile() except Exception: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic @@ -500,6 +501,12 @@ class PosOrder(models.Model): # It may be interesting to have the Traceback logged anyway # for debugging and support purposes _logger.exception('Reconciliation did not work for order %s', order.name) + for move in self.mapped('account_move'): + partial_reconcile = self.env['account.partial.reconcile'].search([ + '|', + ('credit_move_id.move_id', '=', move.id), + ('debit_move_id.move_id', '=', move.id)], limit=1) + partial_reconcile.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[move]) def _filtered_for_reconciliation(self): filter_states = ['invoiced', 'done'] diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index a38505ab06a..d4fc1d9bcf9 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -274,6 +274,7 @@ class PosSession(models.Model): for session in self: company_id = session.config_id.company_id.id ctx = dict(self.env.context, force_company=company_id, company_id=company_id) + ctx_notrack = dict(ctx, mail_notrack=True) for st in session.statement_ids: if abs(st.difference) > st.journal_id.amount_authorized_diff: # The pos manager can close statements with maximums. @@ -281,7 +282,7 @@ class PosSession(models.Model): raise UserError(_("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) if (st.journal_id.type not in ['bank', 'cash']): raise UserError(_("The type of the journal for your payment method should be bank or cash ")) - st.with_context(ctx).sudo().button_confirm_bank() + st.with_context(ctx_notrack).sudo().button_confirm_bank() self.with_context(ctx)._confirm_orders() self.write({'state': 'closed'}) return { diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 9003644ecb6..770fb443fe1 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -155,24 +155,50 @@ class StockMove(models.Model): @api.model def _get_in_base_domain(self, company_id=False): + # Domain: + # - state is done + # - coming from a location without company, or an inventory location within the same company + # - going to a location within the same company domain = [ ('state', '=', 'done'), - ('location_id.company_id', '=', False), - ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id) + '&', + '|', + ('location_id.company_id', '=', False), + '&', + ('location_id.usage', 'in', ['inventory', 'production']), + ('location_id.company_id', '=', company_id or self.env.user.company_id.id), + ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), ] return domain @api.model def _get_all_base_domain(self, company_id=False): + # Domain: + # - state is done + # Then, either 'in' or 'out' moves. + # 'in' moves: + # - coming from a location without company, or an inventory location within the same company + # - going to a location within the same company + # 'out' moves: + # - coming from to a location within the same company + # - going to a location without company, or an inventory location within the same company domain = [ ('state', '=', 'done'), '|', '&', - ('location_id.company_id', '=', False), + '|', + ('location_id.company_id', '=', False), + '&', + ('location_id.usage', 'in', ['inventory', 'production']), + ('location_id.company_id', '=', company_id or self.env.user.company_id.id), ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), '&', ('location_id.company_id', '=', company_id or self.env.user.company_id.id), - ('location_dest_id.company_id', '=', False) + '|', + ('location_dest_id.company_id', '=', False), + '&', + ('location_dest_id.usage', '=', 'inventory'), + ('location_dest_id.company_id', '=', company_id or self.env.user.company_id.id), ] return domain diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 2c6f358d7a4..cc6ff2f4470 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -14,6 +14,7 @@ class TestStockValuation(TransactionCase): self.stock_location = self.env.ref('stock.stock_location_stock') self.customer_location = self.env.ref('stock.stock_location_customers') self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.inventory_location = self.env.ref('stock.location_inventory') self.partner = self.env['res.partner'].create({'name': 'xxx'}) self.owner1 = self.env['res.partner'].create({'name': 'owner1'}) self.uom_unit = self.env.ref('uom.product_uom_unit') @@ -3905,3 +3906,47 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 100) self.assertEqual(self.product1.qty_at_date, 10) self.assertEqual(self.product1.stock_value, 100) + + def test_inventory_fifo_1(self): + """ Make an inventory from a location with a company set, and ensure the product has a stock + value. When the product is sold, ensure there is no remaining quantity on the original move + and no stock value. + """ + self.product1.standard_price = 15 + self.product1.product_tmpl_id.cost_method = 'fifo' + self.inventory_location.company_id = self.env.user.company_id.id + + # Start Inventory: 12 units + move1 = self.env['stock.move'].create({ + 'name': 'Adjustment of 12 units', + 'location_id': self.inventory_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 12.0, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 12.0 + move1._action_done() + + self.assertAlmostEqual(move1.value, 180.0) + self.assertAlmostEqual(move1.remaining_qty, 12.0) + self.assertAlmostEqual(self.product1.stock_value, 180.0) + + # Sell the 12 units + move2 = self.env['stock.move'].create({ + 'name': 'Sell 12 units', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 12.0, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 12.0 + move2._action_done() + + self.assertAlmostEqual(move1.remaining_qty, 0.0) + self.assertAlmostEqual(self.product1.stock_value, 0.0) diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 434750c9250..4e7b1d69c9b 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -20,7 +20,7 @@ class CustomerPortal(CustomerPortal): except exceptions.AccessError: pass else: - if order_sudo.template_id and order_sudo.template_id.active: + if order_sudo.template_id and order_sudo.template_id.active and order_sudo.state in ['draft', 'sent']: return request.redirect('/quote/%s/%s' % (order, access_token or '')) return super(CustomerPortal, self).portal_order_page(order=order, access_token=access_token, **kw) diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 101b87fee49..098a8629e15 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -98,11 +98,26 @@ class SaleOrder(models.Model): 'pricelist': order.pricelist_id.id, }) product = self.env['product.product'].with_context(product_context).browse(product_id) - pu = product.price - if order.pricelist_id and order.partner_id: - order_line = order._cart_find_product_line(product.id) - if order_line: - pu = self.env['account.tax']._fix_tax_included_price_company(pu, product.taxes_id, order_line[0].tax_id, self.company_id) + discount = 0 + + if order.pricelist_id.discount_policy == 'without_discount': + # This part is pretty much a copy-paste of the method '_onchange_discount' of + # 'sale.order.line'. + price, rule_id = order.pricelist_id.with_context(product_context).get_product_price_rule(product, qty or 1.0, order.partner_id) + pu, currency_id = request.env['sale.order.line'].with_context(product_context)._get_real_price_currency(product, rule_id, qty, product.uom_id, order.pricelist_id.id) + if pu != 0: + if order.pricelist_id.currency_id.id != currency_id: + # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency + pu = request.env['res.currency'].browse(currency_id).with_context(product_context).compute(pu, order.pricelist_id.currency_id) + discount = (pu - price) / pu * 100 + if discount < 0: + discount = 0 + else: + pu = product.price + if order.pricelist_id and order.partner_id: + order_line = order._cart_find_product_line(product.id) + if order_line: + pu = self.env['account.tax']._fix_tax_included_price_company(pu, product.taxes_id, order_line[0].tax_id, self.company_id) return { 'product_id': product_id, @@ -110,6 +125,7 @@ class SaleOrder(models.Model): 'order_id': order_id, 'product_uom': product.uom_id.id, 'price_unit': pu, + 'discount': discount, } @api.multi diff --git a/odoo/addons/base/models/res_bank.py b/odoo/addons/base/models/res_bank.py index 613335efd99..ef2f7cfc5c3 100644 --- a/odoo/addons/base/models/res_bank.py +++ b/odoo/addons/base/models/res_bank.py @@ -57,7 +57,7 @@ class ResPartnerBank(models.Model): _name = 'res.partner.bank' _rec_name = 'acc_number' _description = 'Bank Accounts' - _order = 'sequence' + _order = 'sequence, id' acc_type = fields.Selection([('bank', 'Normal')], compute='_compute_acc_type', string='Type', help='Bank account type: Normal or IBAN. Inferred from the bank account number.') acc_number = fields.Char('Account Number', required=True) @@ -67,7 +67,7 @@ class ResPartnerBank(models.Model): bank_id = fields.Many2one('res.bank', string='Bank') bank_name = fields.Char(related='bank_id.name') bank_bic = fields.Char(related='bank_id.bic') - sequence = fields.Integer() + sequence = fields.Integer(default=10) currency_id = fields.Many2one('res.currency', string='Currency') company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.user.company_id, ondelete='cascade') diff --git a/odoo/addons/base/views/res_bank_views.xml b/odoo/addons/base/views/res_bank_views.xml index 1df3b1cfa58..539fee25833 100644 --- a/odoo/addons/base/views/res_bank_views.xml +++ b/odoo/addons/base/views/res_bank_views.xml @@ -64,6 +64,7 @@
+ @@ -83,7 +84,7 @@ res.partner.bank - +