[MERGE] forward port branch 12.0 up to c9913152ec

This commit is contained in:
Denis Ledoux
2019-04-26 18:14:18 +02:00
50 changed files with 1125 additions and 118 deletions
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="ir.config_parameter" id="show_effect">
<record model="ir.config_parameter" id="show_effect" forcecreate="False">
<field name="key">base_setup.show_effect</field>
<field name="value">True</field>
</record>
+2 -2
View File
@@ -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 -1
View File
@@ -365,7 +365,7 @@ class HrExpense(models.Model):
# get move line values
move_line_values = move_line_values_by_expense.get(expense.id)
move_line_dst = move_line_values[-1]
total_amount = abs(move_line_dst['debit'])
total_amount = move_line_dst['debit'] or -move_line_dst['credit']
total_amount_currency = move_line_dst['amount_currency']
# create one more move line, a counterline for the total on payable account
+11 -7
View File
@@ -323,19 +323,22 @@ class HolidaysRequest(models.Model):
@api.onchange('holiday_type')
def _onchange_type(self):
if self.holiday_type == 'employee' and not self.employee_id:
self.employee_id = self.env.user.employee_ids[:1].id
if self.holiday_type == 'employee':
if not self.employee_id:
self.employee_id = self.env.user.employee_ids[:1].id
self.mode_company_id = False
self.category_id = False
elif self.holiday_type == 'company' and not self.mode_company_id:
elif self.holiday_type == 'company':
self.employee_id = False
self.mode_company_id = self.env.user.company_id.id
if not self.mode_company_id:
self.mode_company_id = self.env.user.company_id.id
self.category_id = False
elif self.holiday_type == 'department' and not self.department_id:
elif self.holiday_type == 'department':
self.employee_id = False
self.mode_company_id = False
self.department_id = self.env.user.employee_ids[:1].department_id.id
self.category_id = False
if not self.department_id:
self.department_id = self.env.user.employee_ids[:1].department_id.id
elif self.holiday_type == 'category':
self.employee_id = False
self.mode_company_id = False
@@ -785,8 +788,9 @@ class HolidaysRequest(models.Model):
responsible = self.env.user
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
company = self.department_id.company_id if self.holiday_type == 'department' else self.employee_id.company_id
responsible = self.env['res.users'].search([
('company_id', '=', self.employee_id.company_id.id),
('company_id', '=', company.id),
('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_user').id)
], limit=1)
elif self.state == 'confirm' or (self.state == 'validate' and self.validation_type == 'no_validation'):
@@ -244,16 +244,26 @@ class HolidaysAllocation(models.Model):
@api.onchange('holiday_type')
def _onchange_type(self):
if self.holiday_type == 'employee' and not self.employee_id:
if self.env.user.employee_ids:
self.employee_id = self.env.user.employee_ids[0]
if self.holiday_type == 'employee':
if not self.employee_id:
self.employee_id = self.env.user.employee_ids[:1].id
self.mode_company_id = False
self.category_id = False
elif self.holiday_type == 'company':
self.employee_id = False
if not self.mode_company_id:
self.mode_company_id = self.env.user.company_id.id
self.category_id = False
elif self.holiday_type == 'department':
if self.env.user.employee_ids:
self.department_id = self.department_id or self.env.user.employee_ids[0].department_id
self.employee_id = None
else:
self.employee_id = None
self.department_id = None
self.employee_id = False
self.mode_company_id = False
self.category_id = False
if not self.department_id:
self.department_id = self.env.user.employee_ids[:1].department_id.id
elif self.holiday_type == 'category':
self.employee_id = False
self.mode_company_id = False
self.department_id = False
@api.onchange('employee_id')
def _onchange_employee(self):
@@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tools import mute_logger
from odoo.tests.common import Form
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
@@ -150,3 +151,25 @@ class TestLeaveRequests(TestHrHolidaysBase):
'date_to': fields.Datetime.from_string('2017-07-11 19:00:00'),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_department_leave(self):
""" Create a department leave """
self.employee_hrmanager.write({'department_id': self.hr_dept.id})
self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)]))
leave_form = Form(self.env['hr.leave'].sudo(self.user_hrmanager))
leave_form.holiday_status_id = self.holidays_type_1
leave_form.holiday_type = 'department'
leave_form.department_id = self.hr_dept
leave = leave_form.save()
leave.action_approve()
member_ids = self.hr_dept.member_ids.ids
self.assertEqual(self.env['hr.leave'].search_count([('employee_id', 'in', member_ids)]), len(member_ids), "Leave should be created for members of department")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_allocation_request(self):
""" Create an allocation request """
# employee should be set to current user
allocation_form = Form(self.env['hr.leave.allocation'].sudo(self.user_employee))
allocation_form.holiday_status_id = self.holidays_type_1
allocation = allocation_form.save()
@@ -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)
+7 -3
View File
@@ -476,9 +476,13 @@ class MailTemplate(models.Model):
report = template.report_template
report_service = report.report_name
if report.report_type not in ['qweb-html', 'qweb-pdf']:
raise UserError(_('Unsupported report type %s found.') % report.report_type)
result, format = report.render_qweb_pdf([res_id])
if report.report_type in ['qweb-html', 'qweb-pdf']:
result, format = report.render_qweb_pdf([res_id])
else:
res = report.render([res_id])
if not res:
raise UserError(_('Unsupported report type %s found.') % report.report_type)
result, format = res
# TODO in trunk, change return format to binary to match message_post expected format
result = base64.b64encode(result)
+11 -8
View File
@@ -376,15 +376,15 @@ class MrpWorkorder(models.Model):
production_move.quantity_done += self.qty_producing
if not self.next_work_order_id:
for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))):
if by_product_move.has_tracking != 'serial':
values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor)
self.env['stock.move.line'].create(values)
elif by_product_move.has_tracking == 'serial':
qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id)
for i in range(0, int(float_round(qty_todo, precision_digits=0))):
values = self._get_byproduct_move_line(by_product_move, 1)
for by_product_move in self._get_byproduct_move_to_update():
if by_product_move.has_tracking != 'serial':
values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor)
self.env['stock.move.line'].create(values)
elif by_product_move.has_tracking == 'serial':
qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id)
for i in range(0, int(float_round(qty_todo, precision_digits=0))):
values = self._get_byproduct_move_line(by_product_move, 1)
self.env['stock.move.line'].create(values)
# Update workorder quantity produced
self.qty_produced += self.qty_producing
@@ -415,6 +415,9 @@ class MrpWorkorder(models.Model):
self.button_finish()
return True
def _get_byproduct_move_to_update(self):
return self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
@api.multi
def _start_nextworkorder(self):
rounding = self.product_id.uom_id.rounding
+10 -1
View File
@@ -24,4 +24,13 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('group_mrp_routings')
def _onchange_group_mrp_routings(self):
self.module_mrp_workorder = self.group_mrp_routings
# If we activate 'MRP Work Orders', it means that we need to install 'mrp_workorder'.
# The opposite is not always true: other modules (such as 'quality_mrp_workorder') may
# depend on 'mrp_workorder', so we should not automatically uninstall the module if 'MRP
# Work Orders' is deactivated.
# Long story short: if 'mrp_workorder' is already installed, we don't uninstall it based on
# group_mrp_routings
if self.group_mrp_routings:
self.module_mrp_workorder = True
elif not self.env['ir.module.module'].search([('name', '=', 'mrp_workorder'), ('state', '=', 'installed')]):
self.module_mrp_workorder = False
+3 -2
View File
@@ -170,18 +170,19 @@ class StockWarehouse(models.Model):
manufacture_steps = vals.get('manufacture_steps', def_values['manufacture_steps'])
code = vals.get('code') or self.code
code = code.replace(' ', '').upper()
company_id = vals.get('company_id', self.company_id.id)
values.update({
'pbm_loc_id': {
'name': _('Pre-Production'),
'active': manufacture_steps in ('pbm', 'pbm_sam'),
'usage': 'internal',
'barcode': code + '-PREPRODUCTION'
'barcode': self._valid_barcode(code + '-PREPRODUCTION', company_id)
},
'sam_loc_id': {
'name': _('Post-Production'),
'active': manufacture_steps == 'pbm_sam',
'usage': 'internal',
'barcode': code + '-POSTPRODUCTION'
'barcode': self._valid_barcode(code + '-POSTPRODUCTION', company_id)
},
})
return values
@@ -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"/>
+2 -3
View File
@@ -360,7 +360,7 @@ class PosOrder(models.Model):
account_analytic=account_analytic)
if res:
line1, line2 = res
line1 = Product._convert_prepared_anglosaxon_line(line1, order.partner_id)
line1 = Product._convert_prepared_anglosaxon_line(line1, line['partner_id'])
insert_data('counter_part', {
'name': line1['name'],
'account_id': line1['account_id'],
@@ -370,7 +370,7 @@ class PosOrder(models.Model):
})
line2 = Product._convert_prepared_anglosaxon_line(line2, order.partner_id)
line2 = Product._convert_prepared_anglosaxon_line(line2, line['partner_id'])
insert_data('counter_part', {
'name': line2['name'],
'account_id': line2['account_id'],
@@ -392,7 +392,6 @@ class PosOrder(models.Model):
def insert_data(data_type, values):
# if have_to_group_by:
values.update({
'partner_id': partner_id,
'move_id': move.id,
})
@@ -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
+1 -1
View File
@@ -37,7 +37,7 @@ class PurchaseBillUnion(models.Model):
NULL as vendor_bill_id, id as purchase_order_id
FROM purchase_order
WHERE
state = 'purchase' AND
state in ('purchase', 'done') AND
invoice_status in ('to invoice', 'no')
)""")
+2 -2
View File
@@ -135,12 +135,12 @@
<button name="print_quotation" string="Print RFQ" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm"/>
<button name="button_approve" type="object" states='to approve' string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/>
<button name="action_view_invoice" string="Create Bill" type="object" class="oe_highlight" context="{'create_bill':True}" attrs="{'invisible': ['|', ('state', 'not in', ('purchase')), ('invoice_status', 'in', ('no', 'invoiced'))]}"/>
<button name="action_view_invoice" string="Create Bill" type="object" class="oe_highlight" context="{'create_bill':True}" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'in', ('no', 'invoiced'))]}"/>
<button name="action_rfq_send" states="sent" string="Re-Send by Email" type="object" context="{'send_rfq':True}"/>
<button name="print_quotation" string="Print RFQ" type="object" states="sent" groups="base.group_user"/>
<button name="button_confirm" type="object" states="draft" string="Confirm Order" id="draft_confirm"/>
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/>
<button name="action_view_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/>
<button name="action_view_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/>
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
<button name="button_done" type="object" string="Lock" states="purchase"/>
@@ -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
+7 -1
View File
@@ -60,7 +60,13 @@ class StockMove(models.Model):
if self.purchase_line_id:
purchase_currency = self.purchase_line_id.currency_id
if purchase_currency != self.company_id.currency_id:
purchase_price_unit = self.purchase_line_id.price_unit
# Do not use price_unit since we want the price tax excluded. And by the way, qty
# is in the UOM of the product, not the UOM of the PO line.
purchase_price_unit = (
self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
if self.purchase_line_id.product_uom_qty
else self.purchase_line_id.price_unit
)
currency_move_valuation = purchase_currency.round(purchase_price_unit * abs(qty))
rslt['credit_line_vals']['amount_currency'] = rslt['credit_line_vals']['credit'] and -currency_move_valuation or currency_move_valuation
rslt['credit_line_vals']['currency_id'] = purchase_currency.id
@@ -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.categ_id.property_cost_method = 'fifo'
self.product1.product_tmpl_id.categ_id.property_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.categ_id.property_cost_method = 'fifo'
self.product1.product_tmpl_id.categ_id.property_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)
@@ -19,5 +19,13 @@
<field name="domain_force">[('resource_id.user_id', '=', user.id)]</field>
<field name="perm_read" eval="False"/>
</record>
<record model="ir.rule" id="resource_calendar_leaves_rule_group_admin_modify">
<field name="name">resource.calendar.leaves: admin modifies global</field>
<field name="model_id" ref="model_resource_calendar_leaves"/>
<field name="groups" eval="[(4, ref('base.group_erp_manager'))]"/>
<field name="domain_force">[('resource_id', '=', False)]</field>
<field name="perm_read" eval="False"/>
</record>
</data>
</odoo>
+5 -4
View File
@@ -650,10 +650,11 @@ class SaleOrder(models.Model):
@api.multi
def action_done(self):
tx = self.sudo().transaction_ids.get_last_transaction()
if tx and tx.state == 'pending' and tx.acquirer_id.provider == 'transfer':
tx._set_transaction_done()
tx.write({'is_processed': True})
for order in self:
tx = order.sudo().transaction_ids.get_last_transaction()
if tx and tx.state == 'pending' and tx.acquirer_id.provider == 'transfer':
tx._set_transaction_done()
tx.write({'is_processed': True})
return self.write({'state': 'done'})
@api.multi
+3 -3
View File
@@ -479,8 +479,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'">
@@ -519,7 +519,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}'
/>
+30 -3
View File
@@ -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 = [];
+13
View File
@@ -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>
+8 -6
View File
@@ -894,7 +894,9 @@ class StockMove(models.Model):
# Read the `reserved_availability` field of the moves out of the loop to prevent unwanted
# cache invalidation when actually reserving the move.
reserved_availability = {move: move.reserved_availability for move in self}
roundings = {move: move.product_id.uom_id.rounding for move in self}
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
rounding = roundings[move]
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP')
if move.location_id.should_bypass_reservation()\
@@ -922,7 +924,7 @@ class StockMove(models.Model):
continue
# If we don't need any quantity, consider the move assigned.
need = missing_reserved_quantity
if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding):
if float_is_zero(need, precision_rounding=rounding):
assigned_moves |= move
continue
# Reserve new quants and create move lines accordingly.
@@ -931,9 +933,9 @@ class StockMove(models.Model):
if available_quantity <= 0:
continue
taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, package_id=forced_package_id, strict=False)
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
if float_is_zero(taken_quantity, precision_rounding=rounding):
continue
if need == taken_quantity:
if float_compare(need, taken_quantity, precision_rounding=rounding) == 0:
assigned_moves |= move
else:
partially_available_moves |= move
@@ -996,12 +998,12 @@ class StockMove(models.Model):
# this case `quantity` is directly the quantity on the quants themselves.
available_quantity = self.env['stock.quant']._get_available_quantity(
move.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
if float_is_zero(available_quantity, precision_rounding=move.product_id.uom_id.rounding):
if float_is_zero(available_quantity, precision_rounding=rounding):
continue
taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id)
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
if float_is_zero(taken_quantity, precision_rounding=rounding):
continue
if need - taken_quantity == 0.0:
if float_is_zero(need - taken_quantity, precision_rounding=rounding):
assigned_moves |= move
break
partially_available_moves |= move
+4 -3
View File
@@ -21,7 +21,7 @@ class MrpStockReport(models.TransientModel):
_description = 'Traceability Report'
@api.model
def _get_move_lines(self, move_lines):
def _get_move_lines(self, move_lines, line_id=None):
lines_seen = move_lines
lines_todo = list(move_lines)
while lines_todo:
@@ -43,7 +43,8 @@ class MrpStockReport(models.TransientModel):
])
else:
continue
lines_todo += list(lines)
if line_id is None or line_id in lines.ids:
lines_todo += list(lines)
lines_seen |= lines
return lines_seen - move_lines
@@ -178,7 +179,7 @@ class MrpStockReport(models.TransientModel):
lines = move_lines
else:
# Traceability in case of consumed in.
lines = self._get_move_lines(move_line)
lines = self._get_move_lines(move_line, line_id=line_id)
for line in lines:
unfoldable = False
if line.consume_line_ids or ( line.lot_id and self._get_move_lines(line) and model != "stock.production.lot"):
+13 -5
View File
@@ -497,40 +497,48 @@ class Warehouse(models.Model):
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
code = vals.get('code') or self.code
code = code.replace(' ', '').upper()
company_id = vals.get('company_id', self.company_id.id)
sub_locations = {
'lot_stock_id': {
'name': _('Stock'),
'active': True,
'usage': 'internal',
'barcode': code + '-STOCK'
'barcode': self._valid_barcode(code + '-STOCK', company_id)
},
'wh_input_stock_loc_id': {
'name': _('Input'),
'active': reception_steps != 'one_step',
'usage': 'internal',
'barcode': code + '-INPUT'
'barcode': self._valid_barcode(code + '-INPUT', company_id)
},
'wh_qc_stock_loc_id': {
'name': _('Quality Control'),
'active': reception_steps == 'three_steps',
'usage': 'internal',
'barcode': code + '-QUALITY'
'barcode': self._valid_barcode(code + '-QUALITY', company_id)
},
'wh_output_stock_loc_id': {
'name': _('Output'),
'active': delivery_steps != 'ship_only',
'usage': 'internal',
'barcode': code + '-OUTPUT'
'barcode': self._valid_barcode(code + '-OUTPUT', company_id)
},
'wh_pack_stock_loc_id': {
'name': _('Packing Zone'),
'active': delivery_steps == 'pick_pack_ship',
'usage': 'internal',
'barcode': code + '-PACKING'
'barcode': self._valid_barcode(code + '-PACKING', company_id)
},
}
return sub_locations
def _valid_barcode(self, barcode, company_id):
location = self.env['stock.location'].with_context(active_test=False).search([
('barcode', '=', barcode),
('company_id', '=', company_id)
])
return not location and barcode
def _create_missing_locations(self, vals):
""" It could happen that the user delete a mandatory location or a
module with new locations was installed after some warehouses creation.
@@ -3,7 +3,7 @@
<templates>
<t t-name="stockReports.buttons">
<button type="button" class='btn btn-primary o_stock-widget-pdf'>PRINT</button>
<button type="button" class='btn btn-primary o_stock-widget-pdf d-none'>PRINT</button>
</t>
<div role="dialog" t-name='stockReports.errorModal' class="modal" id="editable_error" tabindex="-1" data-backdrop="static" style="z-index:9999;">
+1 -1
View File
@@ -71,7 +71,7 @@
<field name="model">stock.move</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" create="0">
<kanban class="o_kanban_mobile">
<field name="product_id"/>
<field name="show_details_visible"/>
<field name="product_uom_qty"/>
+3 -1
View File
@@ -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,8 +48,21 @@ class TestDropship(common.TransactionCase):
so.order_line.product_uom_qty = 2.00
self.assertAlmostEqual(po_line.product_qty, 2.00)
class TestDropship(common.TransactionCase):
# 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):
+1 -1
View File
@@ -412,7 +412,7 @@ responses":
.. note::
By defaut, tests cases don't load templates either. We had not
By default, tests cases don't load templates either. We had not
needed to perform any template rendering before here, so we must
now enable templates loading via :js:attr:`the corresponding
option <TestOptions.templates>`.
@@ -2754,7 +2754,8 @@ var TempusDominusBootstrap4 = function ($) {
if ($target.length === 0) {
return;
}
if (config._options.debug || window.debug) {
// /!\ ODOO FIX: check on 'config' existence added by odoo
if (config && config._options.debug || window.debug) {
return;
}
TempusDominusBootstrap4._jQueryInterface.call($target, 'hide', event);
@@ -15,6 +15,7 @@ var DateWidget = Widget.extend({
'change.datetimepicker': 'changeDatetime',
'change .o_datepicker_input': 'changeDatetime',
'input input': '_onInput',
'keydown': '_onKeydown',
'show.datetimepicker': '_onDateTimePickerShow',
},
/**
@@ -239,6 +240,17 @@ var DateWidget = Widget.extend({
this.$input.select();
}
},
/**
* @private
* @param {KeyEvent} ev
*/
_onKeydown: function (ev) {
if (ev.which === $.ui.keyCode.ESCAPE) {
this.__libInput++;
this.$el.datetimepicker('hide');
this.__libInput--;
}
},
/**
* Prevents 'input' events triggered by the library to bubble up, as they
* might have unwanted effects (like triggering 'field_changed' events in
+34 -1
View File
@@ -278,7 +278,40 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('editable list datetimepicker destroy widget', function (assert) {
QUnit.test('editable list datetimepicker destroy widget (edition)', function (assert) {
assert.expect(6);
var done = assert.async();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="date"/>' +
'</tree>',
});
list.$el.on({
'show.datetimepicker': function () {
assert.containsOnce(list, '.o_selected_row');
assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
list.$('input.o_datepicker_input').trigger($.Event('keydown', {which: $.ui.keyCode.ESCAPE}));
assert.containsNone(list, '.o_selected_row');
assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
done();
list.destroy();
}
});
assert.containsN(list, '.o_data_row', 4);
assert.containsNone(list, '.o_selected_row');
testUtilsDom.click(list.$('.o_data_cell:first'));
});
QUnit.test('editable list datetimepicker destroy widget (new line)', function (assert) {
assert.expect(7);
var done = assert.async();
+4 -2
View File
@@ -35,10 +35,12 @@ class ResUsers(models.Model):
@api.model
def _signup_create_user(self, values):
new_user = super(ResUsers, self)._signup_create_user(values)
current_website = self.env['website'].get_current_website()
if request and current_website.specific_user_account:
new_user.website_id = current_website
values['company_id'] = current_website.company_id.id
values['company_ids'] = [(4, current_website.company_id.id)]
values['website_id'] = current_website.id
new_user = super(ResUsers, self)._signup_create_user(values)
return new_user
@api.model
@@ -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
+1
View File
@@ -605,6 +605,7 @@ class WebsiteSale(ProductConfiguratorController):
new_values['customer'] = True
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id
new_values['company_id'] = request.website.company_id.id
new_values['website_id'] = request.website.id
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
+3
View File
@@ -133,7 +133,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:
@@ -0,0 +1,242 @@
odoo.define('website_sale_comparison.tour_comparison', function (require) {
'use strict';
var tour = require('web_tour.tour');
var rpc = require('web.rpc');
tour.register('product_comparison', {
test: true,
url: "/shop",
}, [
// test from shop page
{
content: "add first product 'Three-Seat Sofa' in a comparison list",
trigger: '.oe_product_cart:contains("Three-Seat Sofa") .o_add_compare',
},
{
content: "check compare button contains one product",
trigger: '.o_product_circle:contains(1)',
run: function () {},
},
{
content: "check popover is closed when only one product",
trigger: 'body:not(:has(.comparator-popover))',
run: function () {},
},
{
content: "add second product 'Conference Chair' in a comparison list",
trigger: '.oe_product_cart:contains("Conference Chair") .o_add_compare',
},
{
content: "check popover is now open and compare button contains two products",
extra_trigger: '.comparator-popover',
trigger: ' .o_product_circle:contains(2)',
run: function () {},
},
{
content: "check products name are correct in the comparelist",
extra_trigger: '.o_product_row:contains("Three-Seat Sofa")',
trigger: '.o_product_row:contains("Conference Chair")',
run: function () {},
},
// test form product page
{
content: "go to product page of customizable desk(with variants)",
trigger: '.oe_product_cart a:contains("Customizable Desk")',
},
{
content: "check compare button is still there and contains 2 products",
extra_trigger: '#product_details',
trigger: '.o_product_circle:contains(2)',
run: function () {},
},
{
content: "check popover is closed after changing page",
trigger: 'body:not(:has(.comparator-popover))',
run: function () {},
},
{
content: "add first variant to comparelist",
trigger: '.o_add_compare_dyn',
},
{
content: "check the comparelist is now open and contains 3rd product with correct variant",
extra_trigger: '.comparator-popover',
trigger: '.o_product_row:contains("Customizable Desk (Steel, White)")',
run: function () {},
},
{
content: "select 2nd variant(Black Color)",
trigger: '.variant_attribute[data-attribute_name="Color"] input[data-value_name="Black"]',
},
{
content: "click on compare button to add in comparison list when variant changed",
extra_trigger: '.carousel-indicators li:eq(1)', // there are 2 images on the black variant
trigger: '.o_add_compare_dyn',
},
{
content: "comparelist contains 4th product with correct variant",
extra_trigger: '.o_product_circle:contains(4)',
trigger: '.o_product_row:contains("Customizable Desk (Steel, Black)")',
run: function () {},
},
{
content: "check limit is not reached",
trigger: ':not(.o_comparelist_limit_warning)',
run: function () {},
},
{
content: "select 3nd variant(Custom)",
trigger: '.variant_attribute[data-attribute_name="Legs"] input[data-value_name="Custom"]',
},
{
content: "click on compare button to add in comparison list when variant changed",
extra_trigger: 'body:not(:has(.carousel-indicators))', // there is 1 image on the custom variant
trigger: '.o_add_compare_dyn',
},
{
content: "check limit is reached",
trigger: '.o_comparelist_limit_warning',
run: function () {},
},
{
content: "click on compare button",
trigger: '.o_comparelist_button a',
},
// test on compare page
{
content: "check 1st product contains correct variant",
trigger: '.o_product_comparison_table:contains("Conference Chair (Steel)")',
run: function () {},
},
{
content: "check 2nd product contains correct variant",
trigger: '.o_product_comparison_table:contains("Customizable Desk (Steel, White)")',
run: function () {},
},
{
content: "check 3rd product is correctly added",
trigger: '.o_product_comparison_table:contains("Customizable Desk (Steel, Black)")',
run: function () {},
},
{
content: "check 4th product is correctly added",
trigger: '.o_product_comparison_table:contains("Three-Seat Sofa")',
run: function () {},
},
{
content: "remove one product from compare table",
trigger: '#o_comparelist_table',
run: function () {
// the content of the link is just a float, and the tour manager considers it invisible
$('#o_comparelist_table td:contains("Customizable Desk (Steel, Black)") .o_comparelist_remove').click();
},
},
{
content: "check customizable table with black variant is removed",
trigger: '#o_comparelist_table:not(:contains("Customizable Desk (Steel, Black)"))',
run: function () {},
},
{
content: "open compare menu",
extra_trigger: 'body:has(.o_product_row:contains("Three-Seat Sofa") .o_remove)',
trigger: '.o_product_panel_header',
},
{
content: "remove product",
trigger: '.o_product_row:contains("Three-Seat Sofa") .o_remove',
},
{
content: "click on compare button to reload",
trigger: '.o_comparelist_button a',
},
{
content: "check product 'Three-Seat Sofa' is removed",
trigger: '#o_comparelist_table:not(:contains("Three-Seat Sofa"))',
run: function () {},
},
{
content: "add product 'Conference Chair' to cart",
trigger: '.product_summary:contains("Conference Chair") .a-submit:contains("Add to Cart")',
},
{
content: "check product correctly added to cart",
trigger: '#cart_products:contains("Conference Chair") .js_quantity[value="1"]',
run: function () {},
},
// test advanced configuration and alternative product
{
content: "create product with newly created attribute and its values and set alternative product",
trigger: 'body',
run: function () {
rpc.query({
model: 'product.attribute',
method: 'create',
args: [{
'name': 'color',
'type': 'color',
'create_variant': 'dynamic',
}],
}).then(function (attributeId) {
rpc.query({
model: 'product.template',
method: 'create',
args: [{
'name': 'Basic Chair',
'is_published': true,
'attribute_line_ids': [[0, 0, {
'attribute_id': attributeId,
'value_ids': [
[0, 0, {'name': 'red', 'attribute_id': attributeId, 'sequence': 1}],
[0, 0, {'name': 'blue', 'attribute_id': attributeId, 'sequence': 2}],
],
}]],
}],
}).then(function (productId) {
rpc.query({
model: 'product.template',
method: 'create',
args: [{
'name': 'Classic Chair',
'is_published': true,
'attribute_line_ids': [[0, 0, {
'attribute_id': attributeId,
'value_ids': [
[0, 0, {'name': 'green', 'attribute_id': attributeId, 'sequence': 3}],
[0, 0, {'name': 'yellow', 'attribute_id': attributeId, 'sequence': 4}],
],
}]],
'alternative_product_ids': [[6, 0, [productId]]],
}],
}).then(function () {
window.location.href = '/shop?search=Classic Chair';
});
});
});
},
},
{
content: "click on product",
trigger: '.oe_product_cart a:contains("Classic Chair")',
},
{
content: "click on compare button",
trigger: '.btn.btn-primary:not(.btn-block):contains("Compare")',
},
{
content: "check product 'Classic Chair' with first variant (green) is on compare page",
trigger: '.o_product_comparison_table:contains("Classic Chair (green)")',
run: function () {},
},
{
content: "check alternative product 'Basic Chair' with first variant (red) is on compare page",
trigger: '.o_product_comparison_table:contains("Basic Chair (red)")',
run: function () {},
},
{
content: "check there is the correct attribute",
trigger: '.Uncategorized:contains("color"):contains("red")',
run: function () {},
},
]);
});
@@ -55,3 +55,10 @@ class TestWebsiteSaleComparison(odoo.tests.TransactionCase):
self.assertFalse(Website0.viewref(test_view_key, raise_if_not_found=False))
# Check that the specific inherited view is correctly removed
self.assertFalse(Website1.viewref(test_view_key, raise_if_not_found=False))
@odoo.tests.tagged('post_install', '-at_install')
class TestUi(odoo.tests.HttpCase):
def test_01_admin_tour_product_comparison(self):
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('product_comparison')", "odoo.__DEBUG__.services['web_tour.tour'].tours.product_comparison.ready", login='admin')
@@ -5,6 +5,7 @@
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_sale_comparison/static/src/scss/website_sale_comparison.scss"/>
<script type="text/javascript" src="/website_sale_comparison/static/src/js/website_sale_comparison.js"></script>
<script type="text/javascript" src="/website_sale_comparison/static/src/js/website_sale_tour_comparison.js"></script>
</xpath>
</template>
+1 -1
View File
@@ -21,7 +21,7 @@
"base.lang_en_GB","English (UK)","en_GB","en_GB","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7"
"base.lang_et_EE","Estonian / Eesti keel","et_EE","et","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","True","1"
"base.lang_fi","Finnish / Suomi","fi_FI","fi","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H.%M.%S","True","1"
"base.lang_fil","Filipino","fil","fil_PH","Left-to-Right","[3,0]",".",",","%m/%d/%y","%H:%M:%S","True","1"
"base.lang_fil","Filipino","fil","fil","Left-to-Right","[3,0]",".",",","%m/%d/%y","%H:%M:%S","True","1"
"base.lang_fr_BE","French (BE) / Français (BE)","fr_BE","fr_BE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1"
"base.lang_fr_CA","French (CA) / Français (CA)","fr_CA","fr_CA","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","True","7"
"base.lang_fr_CH","French (CH) / Français (CH)","fr_CH","fr_CH","Left-to-Right","[3,0]",".","'","%d. %m. %Y","%H:%M:%S","True","1"
1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format translatable week_start
21 base.lang_en_GB English (UK) en_GB en_GB Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S True 7
22 base.lang_et_EE Estonian / Eesti keel et_EE et Left-to-Right [3,0] ,   %d.%m.%Y %H:%M:%S True 1
23 base.lang_fi Finnish / Suomi fi_FI fi Left-to-Right [3,0] ,   %d.%m.%Y %H.%M.%S True 1
24 base.lang_fil Filipino fil fil_PH fil Left-to-Right [3,0] . , %m/%d/%y %H:%M:%S True 1
25 base.lang_fr_BE French (BE) / Français (BE) fr_BE fr_BE Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S True 1
26 base.lang_fr_CA French (CA) / Français (CA) fr_CA fr_CA Left-to-Right [3,0] , %Y-%m-%d %H:%M:%S True 7
27 base.lang_fr_CH French (CH) / Français (CH) fr_CH fr_CH Left-to-Right [3,0] . ' %d. %m. %Y %H:%M:%S True 1
@@ -165,6 +165,7 @@
<field name="name">ARS</field>
<field name="symbol">$</field>
<field name="rounding">0.01</field>
<field name="position">before</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Peso</field>
<field name="currency_subunit_label">Centavos</field>