[MERGE] forward port branch saas-11.3 up to 6fd637e131
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}'
|
||||
/>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user