[MERGE] forward port branch 12.0 up to c9913152ec
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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
)""")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}'
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user