summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorDenis Ledoux <dle@odoo.com>2019-04-25 15:47:09 +0200
committerDenis Ledoux <dle@odoo.com>2019-04-25 15:47:09 +0200
commit7228c10884aaa4b4fdd5d5c21b08dbca8fca016e (patch)
treee5d85fefa87c42e2dbf76ab7c2519a5586cbbf50 /addons
parent101a28b8dc444e7cea5ae8e2149012dd5671109b (diff)
parent6fd637e13190909137586c475048b32716024df1 (diff)
[MERGE] forward port branch saas-11.3 up to 6fd637e1319
Diffstat (limited to 'addons')
-rw-r--r--addons/auth_signup/controllers/main.py4
-rw-r--r--addons/base_import_module/models/ir_module.py6
-rw-r--r--addons/event_sale/models/sale_order.py4
-rw-r--r--addons/l10n_fr_pos_cert/models/__init__.py1
-rw-r--r--addons/l10n_fr_pos_cert/models/account_fiscal_position.py19
-rw-r--r--addons/point_of_sale/data/point_of_sale_data.xml5
-rw-r--r--addons/point_of_sale/views/account_journal_view.xml3
-rw-r--r--addons/purchase_stock/models/account_invoice.py4
-rw-r--r--addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py13
-rw-r--r--addons/purchase_stock/tests/test_stockvaluation.py440
-rw-r--r--addons/sale/views/sale_portal_templates.xml6
-rw-r--r--addons/sale_management/controllers/portal.py33
-rw-r--r--addons/sale_management/static/src/js/sale_management.js150
-rw-r--r--addons/sale_mrp/models/sale_mrp.py13
-rw-r--r--addons/sale_quotation_builder/views/sale_order_views.xml7
-rw-r--r--addons/stock_dropshipping/models/purchase.py4
-rw-r--r--addons/stock_dropshipping/tests/test_dropship.py16
-rw-r--r--addons/website_event_sale/models/sale_order.py2
-rw-r--r--addons/website_sale/models/sale_order.py3
19 files changed, 682 insertions, 51 deletions
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index cffc8f00fd5..dbf6c2fec53 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -44,7 +44,7 @@ class AuthSignupHome(Home):
lang=user_sudo.lang,
auth_login=werkzeug.url_encode({'auth_login': user_sudo.email}),
).send_mail(user_sudo.id, force_send=True)
- return super(AuthSignupHome, self).web_login(*args, **kw)
+ return self.web_login(*args, **kw)
except UserError as e:
qcontext['error'] = e.name or e.value
except (SignupError, AssertionError) as e:
@@ -69,7 +69,7 @@ class AuthSignupHome(Home):
try:
if qcontext.get('token'):
self.do_signup(qcontext)
- return super(AuthSignupHome, self).web_login(*args, **kw)
+ return self.web_login(*args, **kw)
else:
login = qcontext.get('login')
assert login, _("No login provided.")
diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index 425ad1a01f4..7fdba9688aa 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -38,6 +38,8 @@ class IrModule(models.Model):
installed_mods = [m.name for m in known_mods if m.state == 'installed']
terp = load_information_from_description_file(module, mod_path=path)
+ if not terp:
+ return False
values = self.get_values_from_terp(terp)
if 'version' in terp:
values['latest_version'] = terp['version']
@@ -133,8 +135,8 @@ class IrModule(models.Model):
try:
# assert mod_name.startswith('theme_')
path = opj(module_dir, mod_name)
- self._import_module(mod_name, path, force=force)
- success.append(mod_name)
+ if self._import_module(mod_name, path, force=force):
+ success.append(mod_name)
except Exception as e:
_logger.exception('Error while importing module')
errors[mod_name] = exception_to_unicode(e)
diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py
index fac1730bead..52017208adc 100644
--- a/addons/event_sale/models/sale_order.py
+++ b/addons/event_sale/models/sale_order.py
@@ -42,11 +42,11 @@ class SaleOrderLine(models.Model):
registrations linked to this line. This method update existing registrations
and create new one for missing one. """
Registration = self.env['event.registration'].sudo()
- registrations = Registration.search([('sale_order_line_id', 'in', self.ids), ('state', '!=', 'cancel')])
+ registrations = Registration.search([('sale_order_line_id', 'in', self.ids)])
for so_line in self.filtered('event_id'):
existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id)
if confirm:
- existing_registrations.filtered(lambda self: self.state != 'open').confirm_registration()
+ existing_registrations.filtered(lambda self: self.state not in ['open', 'cancel']).confirm_registration()
if cancel_to_draft:
existing_registrations.filtered(lambda self: self.state == 'cancel').do_draft()
diff --git a/addons/l10n_fr_pos_cert/models/__init__.py b/addons/l10n_fr_pos_cert/models/__init__.py
index 76de40a9e62..33657992e1c 100644
--- a/addons/l10n_fr_pos_cert/models/__init__.py
+++ b/addons/l10n_fr_pos_cert/models/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_bank_statement
+from . import account_fiscal_position
from . import res_company
from . import pos
diff --git a/addons/l10n_fr_pos_cert/models/account_fiscal_position.py b/addons/l10n_fr_pos_cert/models/account_fiscal_position.py
new file mode 100644
index 00000000000..71b4b12155a
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/account_fiscal_position.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AccountFiscalPosition(models.Model):
+ _inherit = "account.fiscal.position"
+
+ def write(self, vals):
+ if "tax_ids" in vals:
+ if self.env["pos.order"].sudo().search_count([("fiscal_position_id", "in", self.ids)]):
+ raise UserError(
+ _(
+ "You cannot modify a fiscal position used in a POS order. "
+ + "You should archive it and create a new one."
+ )
+ )
+ return super(AccountFiscalPosition, self).write(vals)
diff --git a/addons/point_of_sale/data/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml
index e35a32fc706..34b75548760 100644
--- a/addons/point_of_sale/data/point_of_sale_data.xml
+++ b/addons/point_of_sale/data/point_of_sale_data.xml
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
- <data noupdate="0">
+ <data noupdate="1">
<record id="seq_picking_type_posout" model="ir.sequence">
<field name="name">Picking POS</field>
<field name="prefix">POS</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
-
+ </data>
+ <data noupdate="0">
<record id="picking_type_posout" model="stock.picking.type">
<field name="name">PoS Orders</field>
<field name="sequence_id" ref="seq_picking_type_posout"/>
diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml
index 94c80cbacef..99e0403306e 100644
--- a/addons/point_of_sale/views/account_journal_view.xml
+++ b/addons/point_of_sale/views/account_journal_view.xml
@@ -5,8 +5,7 @@
<field name="res_model">account.journal</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
- <field name="context" eval="{'default_journal_user': 1, 'default_type': 'cash'}"/>
- <field name="domain">[('journal_user','=', 1)]</field>
+ <field name="context" eval="{'search_default_available_in_pos': True, 'default_journal_user': True, 'default_type': 'cash'}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new payment method
diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py
index efb494777cd..048853d4c64 100644
--- a/addons/purchase_stock/models/account_invoice.py
+++ b/addons/purchase_stock/models/account_invoice.py
@@ -75,13 +75,15 @@ class AccountInvoice(models.Model):
for val_stock_move in valuation_stock_move:
# In case val_stock_move is a return move, its valuation entries have been made with the
# currency rate corresponding to the original stock move
- valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date_expected
+ valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date
valuation_price_unit_total += company_currency._convert(
abs(val_stock_move.price_unit) * val_stock_move.product_qty,
inv.currency_id,
company=inv.company_id, date=valuation_date, round=False,
)
valuation_total_qty += val_stock_move.product_qty
+
+ # in Stock Move, price unit is in company_currency
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
line_quantity = valuation_total_qty
diff --git a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
index 5b466f75078..a15190cddfb 100644
--- a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
+++ b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
@@ -62,8 +62,10 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive the goods first, and then make the invoice.
"""
test_product = self.test_product_delivery
- purchase_order = self._create_purchase(test_product, '2018-01-01')
- self._process_pickings(purchase_order.picking_ids)
+ date_po_and_delivery = '2018-01-01'
+
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery)
+ self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
self.env['res.currency.rate'].create({
@@ -149,8 +151,9 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
"""
test_product = self.test_product_delivery
- purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
- self._process_pickings(purchase_order.picking_ids, quantity=2.0)
+ date_po_and_delivery0 = '2017-01-01'
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery0, quantity=5.0)
+ self._process_pickings(purchase_order.picking_ids, quantity=2.0, date=date_po_and_delivery0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
@@ -183,6 +186,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'rate': 12.195747002,
'name': '2017-04-01',
})
+ # We don't need to make the date of processing explicit since the very last rate
+ # will be taken
self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking)
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
index 336cb7ebd7a..73df7e8c22f 100644
--- a/addons/purchase_stock/tests/test_stockvaluation.py
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -3,6 +3,7 @@
import time
from datetime import datetime
+from unittest.mock import patch
from odoo import fields
from odoo.tests import Form
@@ -144,6 +145,7 @@ class TestStockValuation(TransactionCase):
the currency rate, validate the receipt and then check that the value of the received goods
is set according to the last currency rate.
"""
+ self.env['res.currency.rate'].search([]).unlink()
usd_currency = self.env.ref('base.USD')
self.env.user.company_id.currency_id = usd_currency.id
@@ -290,6 +292,9 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.partner_id = self.env.ref('base.res_partner_1')
self.product1 = self.env.ref('product.product_product_8')
Account = self.env['account.account']
+ self.usd_currency = self.env.ref('base.USD')
+ self.eur_currency = self.env.ref('base.EUR')
+
self.stock_input_account = Account.create({
'name': 'Stock Input',
'code': 'StockIn',
@@ -307,6 +312,11 @@ class TestStockValuationWithCOA(AccountingTestCase):
'code': 'Stock Valuation',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
+ self.price_diff_account = Account.create({
+ 'name': 'price diff account',
+ 'code': 'price diff account',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
self.stock_journal = self.env['account.journal'].create({
'name': 'Stock Journal',
'code': 'STJTEST',
@@ -324,12 +334,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
- price_diff_account = self.env['account.account'].create({
- 'name': 'price diff account',
- 'code': 'price diff account',
- 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
- })
- self.product1.property_account_creditor_price_difference = price_diff_account
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
# Receive 10@10 ; create the vendor bill
po1 = self.env['purchase.order'].create({
@@ -416,7 +421,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
creditnote_po2.action_invoice_open()
# check the anglo saxon entries
- price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
+ price_diff_entry = self.env['account.move.line'].search([('account_id', '=', self.price_diff_account.id)])
self.assertEqual(price_diff_entry.credit, 100)
def test_anglosaxon_valuation(self):
@@ -424,12 +429,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
- price_diff_account = self.env['account.account'].create({
- 'name': 'price diff account',
- 'code': 'price diff account',
- 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
- })
- self.product1.property_account_creditor_price_difference = price_diff_account
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
@@ -458,12 +458,422 @@ class TestStockValuationWithCOA(AccountingTestCase):
invoice.action_invoice_open()
# Check what was posted in the price difference account
- price_diff_aml = self.env['account.move.line'].search([('account_id','=',price_diff_account.id)])
+ price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
self.assertAlmostEquals(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)")
# Check what was posted in stock input account
- input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)])
+ input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 10, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.")
+
+ def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The PO is invoiced on the same date as its creation.
+ This shouldn't create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+
+ date_po = '2019-01-01'
+
+ # SetUp product
+ self.product1.product_tmpl_id.cost_method = 'average'
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+ self.product1.product_tmpl_id.purchase_method = 'purchase'
+
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+ # SetUp currency and rates
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.0,
+ 'currency_id': self.usd_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 1.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': date_po,
+ }),
+ ],
+ })
+ po.button_confirm()
+
+ inv = self.env['account.invoice'].create({
+ 'type': 'in_invoice',
+ 'date_invoice': date_po,
+ 'currency_id': self.eur_currency.id,
+ 'purchase_id': po.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'Test',
+ 'price_subtotal': 100.0,
+ 'price_unit': 100.0,
+ 'product_id': self.product1.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': po.order_line.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ })]
+ })
+
+ inv.action_invoice_open()
+
+ move_lines = inv.move_id.line_ids
+ self.assertEqual(len(move_lines), 2)
+
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -100.0)
+ self.assertAlmostEqual(payable_line.balance, -66.67)
+
+ stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.amount_currency, 100.0)
+ self.assertAlmostEqual(stock_line.balance, 66.67)
+
+ def test_realtime_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The PO is invoiced at a later date than its creation.
+ This should create a price difference entry for standard cost method
+ Not for average cost method though, since the PO and invoice have the same currency
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+
+ date_po = '2019-01-01'
+ date_invoice = '2019-01-16'
+
+ # SetUp product Average
+ self.product1.product_tmpl_id.write({
+ 'cost_method': 'average',
+ 'valuation': 'real_time',
+ 'purchase_method': 'purchase',
+ 'property_account_creditor_price_difference': self.price_diff_account.id,
+ })
+ self.product1.invoice_policy = 'order'
+
+ # SetUp product Standard
+ # should have bought at 60 USD
+ # actually invoiced at 70 EUR > 35 USD
+ product_standard = self.product1.product_tmpl_id.copy({
+ 'cost_method': 'standard',
+ 'name': 'Standard Val',
+ 'standard_price': 60,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ }).product_variant_id
+
+ # SetUp currency and rates
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.0,
+ 'currency_id': self.usd_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO
+ def _today(*args, **kwargs):
+ return date_po
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 1.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': date_po,
+ }),
+ (0, 0, {
+ 'name': product_standard.name,
+ 'product_id': product_standard.id,
+ 'product_qty': 1.0,
+ 'product_uom': product_standard.uom_po_id.id,
+ 'price_unit': 40.0,
+ 'date_planned': date_po,
+ }),
+ ],
+ })
+ po.button_confirm()
+
+ line_product_average = po.order_line.filtered(lambda l: l.product_id == self.product1)
+ line_product_standard = po.order_line.filtered(lambda l: l.product_id == product_standard)
+
+ inv = self.env['account.invoice'].create({
+ 'type': 'in_invoice',
+ 'date_invoice': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'purchase_id': po.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'price_subtotal': 100.0,
+ 'price_unit': 100.0,
+ 'product_id': self.product1.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': line_product_average.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ }),
+ (0, 0, {
+ 'name': product_standard.name,
+ 'price_subtotal': 70.0,
+ 'price_unit': 70.0,
+ 'product_id': product_standard.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': line_product_standard.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_invoice_open()
+
+ for p in patchers:
+ p.stop()
+
+ move_lines = inv.move_id.line_ids
+ self.assertEqual(len(move_lines), 4)
+
+ # Ensure no exchange difference move has been created
+ self.assertTrue(all([not l.reconciled for l in move_lines]))
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -170.0)
+ self.assertAlmostEqual(payable_line.balance, -85.00)
+
+ # PRODUCTS CHECKS
+
+ # NO EXCHANGE DIFFERENCE (average)
+ # We ordered for a value of 100 EUR
+ # But by the time we are invoiced for it
+ # the foreign currency appreciated from 1.5 to 2.0
+ # We still have to pay 100 EUR, which now values at 50 USD
+ product_lines = move_lines.filtered(lambda l: l.product_id == self.product1)
+
+ # Stock-wise, we have been invoiced 100 EUR, and we ordered 100 EUR
+ # there is no price difference
+ # However, 100 EUR should be converted at the time of the invoice
+ stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.amount_currency, 100.00)
+ self.assertAlmostEqual(stock_line.balance, 50.00)
+
+ # PRICE DIFFERENCE (STANDARD)
+ # We ordered a product that should have cost 60 USD (120 EUR)
+ # However, we effectively got invoiced 70 EUR (35 USD)
+ product_lines = move_lines.filtered(lambda l: l.product_id == product_standard)
+
+ stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.amount_currency, 120.00)
+ self.assertAlmostEqual(stock_line.balance, 60.00)
+
+ price_diff_line = product_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertEqual(price_diff_line.amount_currency, -50.00)
+ self.assertAlmostEqual(price_diff_line.balance, -25.00)
+
+ def test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The delivery occurs in between PO validation and invoicing
+ The invoice is created at an even different date
+ This should create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+
+ date_po = '2019-01-01'
+ date_delivery = '2019-01-08'
+ date_invoice = '2019-01-16'
+
+ product_avg = self.product1.product_tmpl_id.copy({
+ 'valuation': 'real_time',
+ 'purchase_method': 'purchase',
+ 'cost_method': 'average',
+ 'name': 'AVG',
+ 'standard_price': 60,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ }).product_variant_id
+ product_avg.invoice_policy = 'order'
+
+ # SetUp currency and rates
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.0,
+ 'currency_id': self.usd_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery,
+ 'rate': 0.7,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO
+ def _today(*args, **kwargs):
+ return date_po
+ # To allow testing validation of Delivery
+ def _now(*args, **kwargs):
+ return date_delivery + ' 01:00:00'
+
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ patch('odoo.fields.Datetime.now', _now),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'product_id': product_avg.id,
+ 'product_qty': 1.0,
+ 'product_uom': product_avg.uom_po_id.id,
+ 'price_unit': 30.0,
+ 'date_planned': date_po,
+ })
+ ],
+ })
+ po.button_confirm()
+
+ line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+
+ picking = po.picking_ids
+ (picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 1.0}))
+
+ picking.button_validate()
+
+ inv = self.env['account.invoice'].create({
+ 'type': 'in_invoice',
+ 'date_invoice': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'purchase_id': po.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_subtotal': 30.0,
+ 'price_unit': 30.0,
+ 'product_id': product_avg.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_invoice_open()
+
+ for p in patchers:
+ p.stop()
+
+ move_lines = inv.move_id.line_ids
+ self.assertEqual(len(move_lines), 2)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -30.0)
+ self.assertAlmostEqual(payable_line.balance, -15.00)
+
+ # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ # We ordered a product at 30 EUR valued at 20 USD
+ # We received it when the exchange rate has appreciated
+ # So, the actualized 20 USD are now 20*1.5/0.7 = 42.86 USD
+ product_lines = move_lines.filtered(lambda l: l.product_id == product_avg)
+
+ # Although those 42.86 USD are just due to the exchange difference
+ stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.journal_id, inv.journal_id)
+ self.assertEqual(stock_line.amount_currency, 30.00)
+ self.assertAlmostEqual(stock_line.balance, 15.00)
+ full_reconcile = stock_line.full_reconcile_id
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_line
+ self.assertEqual(len(reconciled_lines), 2)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -30.00)
+ self.assertAlmostEqual(stock_journal_line.balance, -42.86)
+
+ exhange_diff_journal = company.currency_exchange_journal_id.exists()
+ exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
+ self.assertEqual(exchange_stock_line.amount_currency, 0.00)
+ self.assertAlmostEqual(exchange_stock_line.balance, 27.86)
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index ca59a2d0c95..640d917689c 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -484,8 +484,8 @@
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
</td>
<td class="text-right">
- <span t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
- <span t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
+ <span class="oe_order_line_price_subtotal" t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
+ <span class="oe_order_line_price_total" t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
@@ -524,7 +524,7 @@
<td><strong>Subtotal</strong></td>
<td class="text-right">
<span
- data-id="total_amount"
+ data-id="total_untaxed"
t-field="sale_order.amount_untaxed"
t-options='{"widget": "monetary","display_currency": sale_order.pricelist_id.currency_id}'
/>
diff --git a/addons/sale_management/controllers/portal.py b/addons/sale_management/controllers/portal.py
index 9547d8e182b..1796beb7223 100644
--- a/addons/sale_management/controllers/portal.py
+++ b/addons/sale_management/controllers/portal.py
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from functools import partial
+
from odoo import http
+from odoo.tools import formatLang
from odoo.exceptions import AccessError, MissingError
from odoo.http import request
from odoo.addons.sale.controllers.portal import CustomerPortal
@@ -11,6 +14,13 @@ class CustomerPortal(CustomerPortal):
@http.route(['/my/orders/<int:order_id>/update_line'], type='json', auth="public", website=True)
def update(self, line_id, remove=False, unlink=False, order_id=None, access_token=None, **post):
+ values = self.update_line_dict(line_id, remove, unlink, order_id, access_token, **post)
+ if values:
+ return [values['order_line_product_uom_qty'], values['order_amount_total']]
+ return values
+
+ @http.route(['/my/orders/<int:order_id>/update_line_dict'], type='json', auth="public", website=True)
+ def update_line_dict(self, line_id, remove=False, unlink=False, order_id=None, access_token=None, input_quantity=False, **kwargs):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except (AccessError, MissingError):
@@ -24,10 +34,27 @@ class CustomerPortal(CustomerPortal):
if unlink:
order_line.unlink()
return False # return False to reload the page, the line must move back to options and the JS doesn't handle it
- number = -1 if remove else 1
- quantity = order_line.product_uom_qty + number
+
+ if input_quantity is not False:
+ quantity = input_quantity
+ else:
+ number = -1 if remove else 1
+ quantity = order_line.product_uom_qty + number
+
+ if quantity < 0:
+ quantity = 0.0
order_line.write({'product_uom_qty': quantity})
- return [str(quantity), str(order_sudo.amount_total)]
+ currency = order_sudo.currency_id
+ format_price = partial(formatLang, request.env, digits=currency.decimal_places)
+
+ return {
+ 'order_line_product_uom_qty': str(quantity),
+ 'order_line_price_total': format_price(order_line.price_total),
+ 'order_line_price_subtotal': format_price(order_line.price_subtotal),
+ 'order_amount_total': format_price(order_sudo.amount_total),
+ 'order_amount_untaxed': format_price(order_sudo.amount_untaxed),
+ 'order_amount_tax': format_price(order_sudo.amount_tax),
+ }
@http.route(["/my/orders/<int:order_id>/add_option/<int:option_id>"], type='http', auth="public", website=True)
def add(self, order_id, option_id, access_token=None, **post):
diff --git a/addons/sale_management/static/src/js/sale_management.js b/addons/sale_management/static/src/js/sale_management.js
index 77806630166..88b956e0e6e 100644
--- a/addons/sale_management/static/src/js/sale_management.js
+++ b/addons/sale_management/static/src/js/sale_management.js
@@ -14,31 +14,155 @@ if (!$('.o_portal_sale_sidebar').length) {
events: {
'click' : 'onClick',
},
- onClick: function (ev) {
- ev.preventDefault();
+ /**
+ * @override
+ */
+ start: function () {
var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.elems = self._getUpdatableElements();
+ self.elems.$lineQuantity.change(function (ev) {
+ var quantity = parseInt(this.value);
+ self._onChangeQuantity(quantity);
+ });
+ });
+ },
+ /**
+ * Process the change in line quantity
+ *
+ * @private
+ * @param {Int} quantity, the new quantity of the line
+ * If not present it will increment/decrement the existing quantity
+ */
+ _onChangeQuantity: function (quantity) {
var href = this.$el.attr("href");
var order_id = href.match(/my\/orders\/([0-9]+)/);
var line_id = href.match(/update_line\/([0-9]+)/);
var params = {
'line_id': line_id[1],
- 'remove': self.$el.is('[href*="remove"]'),
- 'unlink': self.$el.is('[href*="unlink"]'),
+ 'remove': this.$el.is('[href*="remove"]'),
+ 'unlink': this.$el.is('[href*="unlink"]'),
+ 'input_quantity': quantity >= 0 ? quantity : false,
};
- var token = href.match(/token=(.*)/);
+ var token = href.match(/token=([\w\d-]*)/)[1];
if (token) {
params.access_token = token;
}
- var url = "/my/orders/" + parseInt(order_id[1]) + "/update_line";
- ajax.jsonRpc(url, 'call', params).then(function (data) {
- if (!data) {
- window.location.reload();
- }
- self.$el.parents('.input-group:first').find('.js_quantity').val(data[0]);
- $('[data-id="total_amount"]>span').html(data[1]);
- });
+
+ this._callUpdateLineRoute(order_id, params).then(this._updateOrderValues.bind(this));
return false;
},
+ /**
+ * Reacts to the click on the -/+ buttons
+ *
+ * @param {Event} ev
+ */
+ onClick: function (ev) {
+ ev.preventDefault();
+ return this._onChangeQuantity();
+ },
+ /**
+ * Calls the route to get updated values of the line and order
+ * when the quantity of a product has changed
+ *
+ * @private
+ * @param {Object} params
+ * @return {Deferred}
+ */
+ _callUpdateLineRoute: function (order_id, params) {
+ var def = new $.Deferred();
+ var url = "/my/orders/" + parseInt(order_id[1]) + "/update_line_dict";
+ ajax.jsonRpc(url, 'call', params)
+ .then(def.resolve.bind(def))
+ .fail(function () {
+ // Compatibility: the server may not have been restarted
+ // So the real route may not exist
+ delete params.input_quantity;
+ ajax.jsonRpc("/quote/update_line", 'call', params)
+ .fail(def.reject.bind(def))
+ .then(function (data) {
+ // Data is an array, convert it to a dict
+ var actualData = data;
+ if (data) {
+ actualData = {
+ order_amount_total: data[1],
+ order_line_product_uom_qty: data[0],
+ };
+ }
+ def.resolve(actualData);
+ });
+ });
+ return def;
+ },
+ /**
+ * Processes data from the server to update the UI
+ *
+ * @private
+ * @param {Object} data: contains order and line updated values
+ */
+ _updateOrderValues: function (data) {
+ if (!data) {
+ window.location.reload();
+ }
+
+ var orderAmountTotal = data.order_amount_total;
+ var orderAmountUntaxed = data.order_amount_untaxed;
+ var orderAmountTax = data.order_amount_tax;
+
+ var lineProductUomQty = data.order_line_product_uom_qty;
+ var linePriceTotal = data.order_line_price_total;
+ var linePriceSubTotal = data.order_line_price_subtotal;
+
+ this.elems.$lineQuantity.val(lineProductUomQty)
+
+ if (this.elems.$linePriceTotal.length && linePriceTotal !== undefined) {
+ this.elems.$linePriceTotal.text(linePriceTotal);
+ }
+ if (this.elems.$linePriceSubTotal.length && linePriceSubTotal !== undefined) {
+ this.elems.$linePriceSubTotal.text(linePriceSubTotal);
+ }
+
+ if (orderAmountUntaxed !== undefined) {
+ this.elems.$orderAmountUntaxed.text(orderAmountUntaxed);
+ }
+
+ if (orderAmountTotal !== undefined) {
+ this.elems.$orderAmountTotal.text(orderAmountTotal);
+ }
+ },
+ /**
+ * Locate in the DOM the elements to update
+ * Mostly for compatibility, when the module has not been upgraded
+ * In that case, we need to fall back to some other elements
+ *
+ * @private
+ * @return {Object}: Jquery elements to update
+ */
+ _getUpdatableElements: function () {
+ var $parentTr = this.$el.parents('tr:first');
+ var $linePriceTotal = $parentTr.find('.oe_order_line_price_total .oe_currency_value');
+ var $linePriceSubTotal = $parentTr.find('.oe_order_line_price_subtotal .oe_currency_value');
+
+ if (!$linePriceTotal.length && !$linePriceSubTotal.length) {
+ $linePriceTotal = $linePriceSubTotal = $parentTr.find('.oe_currency_value').last();
+ }
+
+ var $orderAmountUntaxed = $('[data-id="total_untaxed"]>span');
+ var $orderAmountTotal = $('[data-id="total_amount"]>span');
+
+ if (!$orderAmountUntaxed.length) {
+ $orderAmountUntaxed = $orderAmountTotal.eq(1);
+ $orderAmountTotal = $orderAmountTotal.eq(0).add($orderAmountTotal.eq(3));
+ }
+
+ return {
+ $lineQuantity: this.$el.parents('.input-group:first').find('.js_quantity'),
+ $linePriceSubTotal: $linePriceSubTotal,
+ $linePriceTotal: $linePriceTotal,
+ $orderAmountUntaxed: $orderAmountUntaxed,
+ $orderAmountTotal: $orderAmountTotal,
+ }
+ }
});
var update_button_list = [];
diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py
index d7738367ef7..a17ef14d11f 100644
--- a/addons/sale_mrp/models/sale_mrp.py
+++ b/addons/sale_mrp/models/sale_mrp.py
@@ -60,6 +60,19 @@ class SaleOrderLine(models.Model):
return self.env.context['previous_product_uom_qty'].get(self.id, 0.0)
return super(SaleOrderLine, self)._get_qty_procurement()
+ @api.multi
+ @api.depends('product_id', 'move_ids.state')
+ def _compute_qty_delivered_method(self):
+ lines = self.env['sale.order.line']
+ for line in self:
+ bom = self.env['mrp.bom']._bom_find(product=line.product_id, company_id=line.company_id.id)
+ if bom and bom.type == 'phantom' and line.order_id.state == 'sale':
+ bom_delivered = all([move.state == 'done' for move in line.move_ids])
+ if not bom_delivered:
+ line.qty_delivered_method = 'manual'
+ lines |= line
+ super(SaleOrderLine, self - lines)._compute_qty_delivered_method()
+
class AccountInvoiceLine(models.Model):
# TDE FIXME: what is this code ??
diff --git a/addons/sale_quotation_builder/views/sale_order_views.xml b/addons/sale_quotation_builder/views/sale_order_views.xml
index e46a0cdd68b..ff2eb495f49 100644
--- a/addons/sale_quotation_builder/views/sale_order_views.xml
+++ b/addons/sale_quotation_builder/views/sale_order_views.xml
@@ -7,6 +7,13 @@
<field name="inherit_id" ref="sale_management.sale_order_form_quote"/>
<field name="arch" type="xml">
+ <xpath expr="//page/field[@name='order_line']/tree/field[@name='name']" position="after">
+ <field name="website_description" invisible="1" readonly="1"/>
+ </xpath>
+ <xpath expr="//page/field[@name='order_line']/form/field[@name='name']" position="after">
+ <field name="website_description" invisible="1" readonly="1"/>
+ </xpath>
+
<xpath expr="//button[@name='button_add_to_order']" position="after">
<field name="website_description" invisible="1"/>
</xpath>
diff --git a/addons/stock_dropshipping/models/purchase.py b/addons/stock_dropshipping/models/purchase.py
index af98e29d390..eef78466366 100644
--- a/addons/stock_dropshipping/models/purchase.py
+++ b/addons/stock_dropshipping/models/purchase.py
@@ -15,7 +15,9 @@ class PurchaseOrderLine(models.Model):
return res
def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values):
- if values.get('route_ids') and values['route_ids'] == self.env.ref('stock_dropshipping.route_drop_shipping'):
+ if values.get('sale_line_id') and values['sale_line_id'] != self.sale_line_id.id:
+ # if this is defined, this is a dropshipping line, so no
+ # this is to correctly map delivered quantities to the so lines
return False
return super(PurchaseOrderLine, self)._merge_in_existing_line(
product_id=product_id, product_qty=product_qty, product_uom=product_uom,
diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py
index af492930a7c..f70897f9eb8 100644
--- a/addons/stock_dropshipping/tests/test_dropship.py
+++ b/addons/stock_dropshipping/tests/test_dropship.py
@@ -48,6 +48,22 @@ class TestDropship(common.TransactionCase):
so.order_line.product_uom_qty = 2.00
self.assertAlmostEqual(po_line.product_qty, 2.00)
+ # Create a new so line
+ sol2 = self.env['sale.order.line'].create({
+ 'order_id': so.id,
+ 'name': prod.name,
+ 'product_id': prod.id,
+ 'product_uom_qty': 3.00,
+ 'product_uom': prod.uom_id.id,
+ 'price_unit': 12,
+ })
+ # there is a new line
+ pol2 = po.order_line - po_line
+ # the first line is unchanged
+ self.assertAlmostEqual(po_line.product_qty, 2.00)
+ # the new line matches the new line on the so
+ self.assertAlmostEqual(pol2.product_qty, sol2.product_uom_qty)
+
def test_00_dropship(self):
# Create a vendor
diff --git a/addons/website_event_sale/models/sale_order.py b/addons/website_event_sale/models/sale_order.py
index 097ce0adbaf..cd25e6e1943 100644
--- a/addons/website_event_sale/models/sale_order.py
+++ b/addons/website_event_sale/models/sale_order.py
@@ -41,7 +41,7 @@ class SaleOrder(models.Model):
values['product_id'] = ticket.product_id.id
values['event_id'] = ticket.event_id.id
values['event_ticket_id'] = ticket.id
- values['price_unit'] = ticket.price_reduce or ticket.price
+ values['price_unit'] = ticket.price_reduce
values['name'] = ticket.get_ticket_multiline_description_sale()
# avoid writing related values that end up locking the product record
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index bec2445e64f..05db43f9f4b 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -127,7 +127,10 @@ class SaleOrder(models.Model):
pu = currency._convert(pu, order.pricelist_id.currency_id, order.company_id, date)
discount = (pu - price) / pu * 100
if discount < 0:
+ # In case the discount is negative, we don't want to show it to the customer,
+ # but we still want to use the price defined on the pricelist
discount = 0
+ pu = price
else:
pu = product.price
if order.pricelist_id and order.partner_id: