[MERGE] forward port branch saas-11.4 up to f6ca72b3ce
@@ -538,6 +538,8 @@ class AccountJournal(models.Model):
|
||||
@api.constrains('currency_id', 'default_credit_account_id', 'default_debit_account_id')
|
||||
def _check_currency(self):
|
||||
if self.currency_id:
|
||||
if self.currency_id == self.company_id.currency_id:
|
||||
raise ValidationError(_("Currency field should only be set if the journal's currency is different from the company's. Leave the field blank to use company currency."))
|
||||
if self.default_credit_account_id and not self.default_credit_account_id.currency_id.id == self.currency_id.id:
|
||||
raise ValidationError(_('The currency of the journal should be the same than the default credit account.'))
|
||||
if self.default_debit_account_id and not self.default_debit_account_id.currency_id.id == self.currency_id.id:
|
||||
|
||||
@@ -276,7 +276,6 @@
|
||||
<field name="type">bank</field>
|
||||
<field name="default_debit_account_id" ref="usd_bnk"/>
|
||||
<field name="default_credit_account_id" ref="usd_bnk"/>
|
||||
<field name="currency_id" ref="base.USD"/>
|
||||
<field name="bank_account_id" ref="bank_account_usd"/>
|
||||
</record>
|
||||
<record id="base.main_company" model="res.company">
|
||||
|
||||
@@ -456,6 +456,7 @@ class TestPayment(AccountingTestCase):
|
||||
#check the invoice status
|
||||
self.assertEqual(invoice.state, 'paid')
|
||||
|
||||
|
||||
def test_payment_and_writeoff_in_other_currency_3(self):
|
||||
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
|
||||
# Invoice made in secondary currency for which the rate to the company currency
|
||||
@@ -536,4 +537,4 @@ class TestPayment(AccountingTestCase):
|
||||
# Check the move has been posted properly
|
||||
self.assertEqual(payment.mapped('move_line_ids.move_id.state'), ['posted'], "After bank reconciliation, the payment's account.move should be posted.")
|
||||
self.assertEqual(payment.mapped('move_line_ids.move_id.date'), stmt_line_date, "After bank reconciliation, the payment's account.move should share the same date as the bank statement.")
|
||||
self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
|
||||
self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
|
||||
@@ -787,7 +787,7 @@ class TestReconciliation(AccountingTestCase):
|
||||
currency = self.env.user.company_id.currency_id
|
||||
|
||||
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
|
||||
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id})
|
||||
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE'})
|
||||
|
||||
statement = self.make_payment(invoice, journal, 50)
|
||||
|
||||
|
||||
@@ -607,13 +607,16 @@ class Meeting(models.Model):
|
||||
event_date = datetime.datetime.now()
|
||||
|
||||
use_naive_datetime = self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule
|
||||
if use_naive_datetime:
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
|
||||
else:
|
||||
if not use_naive_datetime:
|
||||
# Convert the event date to saved timezone (or context tz) as it'll
|
||||
# define the correct hour/day asked by the user to repeat for recurrence.
|
||||
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
|
||||
event_date = event_date.astimezone(timezone)
|
||||
|
||||
# The start date is naive
|
||||
# the timezone will be applied, if necessary, at the very end of the process
|
||||
# to allow for DST timezone reevaluation
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
|
||||
|
||||
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
|
||||
|
||||
# We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to
|
||||
@@ -629,12 +632,15 @@ class Meeting(models.Model):
|
||||
else:
|
||||
if not recurring_date.tzinfo:
|
||||
recurring_date = pytz.UTC.localize(recurring_date)
|
||||
recurring_date = recurring_date.astimezone(timezone)
|
||||
recurring_date = recurring_date.astimezone(timezone).replace(tzinfo=None)
|
||||
if date_field == "stop":
|
||||
recurring_date += timedelta(hours=self.duration)
|
||||
rset1.exdate(recurring_date)
|
||||
invalidate = True
|
||||
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR]
|
||||
|
||||
def naive_tz_to_utc(d):
|
||||
return timezone.localize(d).astimezone(pytz.UTC)
|
||||
return [naive_tz_to_utc(d) if not use_naive_datetime else d for d in rset1 if d.year < MAXYEAR]
|
||||
|
||||
@api.multi
|
||||
def _get_recurrency_end_date(self):
|
||||
@@ -893,17 +899,17 @@ class Meeting(models.Model):
|
||||
def _inverse_dates(self):
|
||||
for meeting in self:
|
||||
if meeting.allday:
|
||||
tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
|
||||
|
||||
# Convention break:
|
||||
# stop and start are NOT in UTC in allday event
|
||||
# in this case, they actually represent a date
|
||||
# i.e. Christmas is on 25/12 for everyone
|
||||
# even if people don't celebrate it simultaneously
|
||||
enddate = fields.Datetime.from_string(meeting.stop_date)
|
||||
enddate = tz.localize(enddate)
|
||||
enddate = enddate.replace(hour=18)
|
||||
enddate = enddate.astimezone(pytz.utc)
|
||||
|
||||
startdate = fields.Datetime.from_string(meeting.start_date)
|
||||
startdate = tz.localize(startdate) # Add "+hh:mm" timezone
|
||||
startdate = startdate.replace(hour=8) # Set 8 AM in localtime
|
||||
startdate = startdate.astimezone(pytz.utc) # Convert to UTC
|
||||
startdate = startdate.replace(hour=8) # Set 8 AM
|
||||
|
||||
meeting.write({
|
||||
'start': startdate.replace(tzinfo=None),
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase
|
||||
import pytz
|
||||
|
||||
|
||||
class TestCalendar(TransactionCase):
|
||||
@@ -275,3 +276,41 @@ class TestCalendar(TransactionCase):
|
||||
self.assertEqual(test_event.res_model, test_record._name)
|
||||
self.assertEqual(test_event.res_id, test_record.id)
|
||||
self.assertEqual(len(test_record.activity_ids), 1)
|
||||
|
||||
def test_event_allday(self):
|
||||
self.env.user.tz = 'Pacific/Honolulu'
|
||||
|
||||
event = self.CalendarEvent.create({
|
||||
'name': 'All Day',
|
||||
'start': "2018-10-16 00:00:00",
|
||||
'start_date': "2018-10-16",
|
||||
'start_datetime': False,
|
||||
'stop': "2018-10-18 00:00:00",
|
||||
'stop_date': "2018-10-18",
|
||||
'stop_datetime': False,
|
||||
'allday': True,
|
||||
})
|
||||
|
||||
self.assertEqual(str(event.start), '2018-10-16 08:00:00')
|
||||
self.assertEqual(str(event.stop), '2018-10-18 18:00:00')
|
||||
|
||||
def test_recurring_around_dst(self):
|
||||
m = self.CalendarEvent.create({
|
||||
'name': "wheee",
|
||||
'start': '2018-10-27 14:30:00',
|
||||
'allday': False,
|
||||
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=4',
|
||||
'duration': 2,
|
||||
'stop': '2018-10-27 16:30:00',
|
||||
})
|
||||
|
||||
start_recurring_dates = m.with_context({'tz': 'Europe/Brussels'})._get_recurrent_date_by_event()
|
||||
self.assertEqual(len(start_recurring_dates), 4)
|
||||
|
||||
for d in start_recurring_dates:
|
||||
self.assertEqual(d.tzinfo, pytz.UTC)
|
||||
if d.day < 28: # DST switch happens between 2018-10-27 and 2018-10-28
|
||||
self.assertEqual(d.hour, 14)
|
||||
else:
|
||||
self.assertEqual(d.hour, 15)
|
||||
self.assertEqual(d.minute, 30)
|
||||
|
||||
@@ -77,7 +77,7 @@ class HrAttendance(models.Model):
|
||||
('employee_id', '=', attendance.employee_id.id),
|
||||
('check_out', '=', False),
|
||||
('id', '!=', attendance.id),
|
||||
])
|
||||
], order='check_in desc', limit=1)
|
||||
if no_check_out_attendances:
|
||||
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s") % {
|
||||
'empl_name': attendance.employee_id.name,
|
||||
|
||||
@@ -11,8 +11,12 @@ import werkzeug.urls
|
||||
def _is_l10n_ch_postal(account_ref):
|
||||
""" Returns True iff the string account_ref is a valid postal account number,
|
||||
i.e. it only contains ciphers and is last cipher is the result of a recursive
|
||||
modulo 10 operation ran over the rest of it.
|
||||
modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
|
||||
"""
|
||||
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', account_ref or ''):
|
||||
ref_subparts = account_ref.split('-')
|
||||
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6,'0') + ref_subparts[2]
|
||||
|
||||
if re.match('\d+$', account_ref or ''):
|
||||
account_ref_without_check = account_ref[:-1]
|
||||
return mod10r(account_ref_without_check) == account_ref
|
||||
|
||||
@@ -1807,7 +1807,7 @@
|
||||
</record>
|
||||
|
||||
<record id="pcg_4661" model="account.account.template">
|
||||
<field name="name">>Datorii din operaţiuni de fiducie</field>
|
||||
<field name="name">Datorii din operaţiuni de fiducie</field>
|
||||
<field name="code">4661</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
|
||||
@@ -81,8 +81,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ
|
||||
</field>
|
||||
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ</field>
|
||||
</record>
|
||||
<record id="chart1332" model="account.account.template">
|
||||
<field name="code">1332</field>
|
||||
@@ -140,7 +139,7 @@
|
||||
</record>
|
||||
<record id="chart1388" model="account.account.template">
|
||||
<field name="code">1388</field>
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="reconcile" eval="True"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Phải thu khác</field>
|
||||
@@ -149,7 +148,7 @@
|
||||
|
||||
<record id="chart141" model="account.account.template">
|
||||
<field name="code">141</field>
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="reconcile" eval="True"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Tạm ứng</field>
|
||||
@@ -322,8 +321,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm
|
||||
</field>
|
||||
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm</field>
|
||||
</record>
|
||||
<record id="chart2118" model="account.account.template">
|
||||
<field name="code">2118</field>
|
||||
@@ -449,7 +447,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">>Đầu tư vào công ty liên doanh</field>
|
||||
<field name="name">Đầu tư vào công ty liên doanh</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -459,7 +457,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">>Đầu tư góp vốn vào đơn vị khác</field>
|
||||
<field name="name">Đầu tư góp vốn vào đơn vị khác</field>
|
||||
</record>
|
||||
|
||||
<record id="chart2288" model="account.account.template">
|
||||
@@ -651,14 +649,14 @@
|
||||
|
||||
<record id="chart3341" model="account.account.template">
|
||||
<field name="code">3341</field>
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="reconcile" eval="True"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Phải trả công nhân viên</field>
|
||||
</record>
|
||||
<record id="chart3348" model="account.account.template">
|
||||
<field name="code">3348</field>
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="reconcile" eval="True"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Phải trả người lao động khác</field>
|
||||
@@ -677,7 +675,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Phải trả nội bộ về vốn kinh doanh </field>
|
||||
<field name="name">Phải trả nội bộ về vốn kinh doanh</field>
|
||||
</record>
|
||||
|
||||
<record id="chart3362" model="account.account.template">
|
||||
@@ -763,7 +761,7 @@
|
||||
</record>
|
||||
<record id="chart3388" model="account.account.template">
|
||||
<field name="code">3388</field>
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="reconcile" eval="True"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Phải trả, phải nộp khác</field>
|
||||
@@ -790,7 +788,7 @@
|
||||
<field name="reconcile" eval="False"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Trái phiếu </field>
|
||||
<field name="name">Trái phiếu</field>
|
||||
</record>
|
||||
<record id="chart34311" model="account.account.template">
|
||||
<field name="code">34311</field>
|
||||
|
||||
@@ -9,6 +9,7 @@ from odoo import api, fields, models, tools
|
||||
class MailTracking(models.Model):
|
||||
_name = 'mail.tracking.value'
|
||||
_description = 'Mail Tracking Value'
|
||||
_rec_name = 'field'
|
||||
_order = 'track_sequence asc'
|
||||
|
||||
# TDE CLEANME: why not a m2o to ir model field ?
|
||||
|
||||
@@ -390,6 +390,7 @@ var BasicComposer = Widget.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
this.$(".o_composer_button_send").prop("disabled", true);
|
||||
clearTimeout(this._cannedTimeout);
|
||||
var self = this;
|
||||
this._preprocessMessage().then(function (message) {
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
</span>
|
||||
</p>
|
||||
<div class="o_thread_message_content">
|
||||
<t t-raw="message.getBody()"/>
|
||||
<t t-if="message.hasTrackingValues()">
|
||||
<t t-if="message.hasSubtypeDescription()">
|
||||
<p><t t-esc="message.getSubtypeDescription()"/></p>
|
||||
@@ -358,9 +359,6 @@
|
||||
<t t-call="mail.widget.Thread.MessageTracking"/>
|
||||
</t>
|
||||
<p t-if="options.displaySubjectOnMessages and message.shouldDisplaySubject()" class="o_mail_subject">Subject: <t t-esc="message.getSubject()"/></p>
|
||||
<t t-if="!(message.hasTrackingValues())">
|
||||
<t t-raw="message.getBody()"/>
|
||||
</t>
|
||||
<t t-if="message.hasAttachments()">
|
||||
<div t-if="message.hasImageAttachments()" class="o_attachments_previews">
|
||||
<t t-foreach="message.getImageAttachments()" t-as="attachment">
|
||||
|
||||
@@ -786,6 +786,59 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('chatter: post a message disable the send button', function(assert) {
|
||||
assert.expect(3);
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="message_ids" widget="mail_thread"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'message_get_suggested_recipients') {
|
||||
return $.when({2: []});
|
||||
}
|
||||
if (args.method === 'message_post') {
|
||||
assert.ok(form.$('.o_composer_button_send').prop("disabled"),
|
||||
"Send button should be disabled when a message is being sent");
|
||||
return $.when(57923);
|
||||
}
|
||||
if (args.method === 'message_format') {
|
||||
return $.when([{
|
||||
author_id: ["42", "Me"],
|
||||
model: 'partner',
|
||||
}]);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
get_messages: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.data.callback($.when([]));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
|
||||
"Send button should be enabled when posting a message");
|
||||
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
|
||||
form.$('.oe_chatter .o_composer_button_send').click();
|
||||
form.$('.o_chatter_button_new_message').click();
|
||||
assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
|
||||
"Send button should be enabled when posting another message");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: receive notif when document is open', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class MrpUnbuild(models.Model):
|
||||
if produce_move.has_tracking != 'none':
|
||||
original_move = self.mo_id.move_raw_ids.filtered(lambda move: move.product_id == produce_move.product_id)
|
||||
needed_quantity = produce_move.product_qty
|
||||
for move_lines in original_move.mapped('move_line_ids'):
|
||||
for move_lines in original_move.mapped('move_line_ids').filtered(lambda ml: ml.lot_produced_id == self.lot_id):
|
||||
# Iterate over all move_lines until we unbuilded the correct quantity.
|
||||
taken_quantity = min(needed_quantity, move_lines.qty_done)
|
||||
if taken_quantity:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="work_order" title="Work Order Operations allow you to create and manage the manufacturing operations that should be followed within your work centers in order to produce a product. They are attached to bills of materials that will define the required raw materials.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_mrp_routings" widget="upgrade_boolean"/>
|
||||
<field name="group_mrp_routings"/>
|
||||
<field name="module_mrp_workorder" invisible="1"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane" id="workorder_settings">
|
||||
|
||||
@@ -53,12 +53,11 @@ class ProductProduct(models.Model):
|
||||
if not boms_to_recompute:
|
||||
boms_to_recompute = []
|
||||
total = 0
|
||||
quant_quantity = bom.product_uom_id._compute_quantity(bom.product_qty, bom.product_tmpl_id.uom_id)
|
||||
for opt in bom.routing_id.operation_ids:
|
||||
duration_expected = (
|
||||
opt.workcenter_id.time_start +
|
||||
opt.workcenter_id.time_stop +
|
||||
quant_quantity * opt.time_cycle)
|
||||
opt.time_cycle)
|
||||
total += (duration_expected / 60) * opt.workcenter_id.costs_hour
|
||||
for line in bom.bom_line_ids:
|
||||
if line._skip_bom_line(self):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common, Form
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.tools.float_utils import float_round, float_compare
|
||||
|
||||
|
||||
class TestBom(common.TransactionCase):
|
||||
@@ -166,11 +166,11 @@ class TestBom(common.TransactionCase):
|
||||
# Table Head Operation Cost (1 Dozen)
|
||||
# --------------------------------------------------------------------------
|
||||
# Operation cost calculate for 1 dozens
|
||||
# Cutting (20 * 12 / 60) * 100 = 400
|
||||
# Drilling (25 * 12 / 60) * 100 = 500
|
||||
# Fitting (30 * 12 / 60) * 100 = 600
|
||||
# Cutting (20 * 1 / 60) * 100 = 33,33
|
||||
# Drilling (25 * 1 / 60) * 100 = 41,67
|
||||
# Fitting (30 * 1 / 60) * 100 = 50
|
||||
# ----------------------------------------
|
||||
# Operation Cost 1 dozen (1500 per dozen) and 125 for 1 Unit
|
||||
# Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
self.bom_2.routing_id = routing_1.id
|
||||
@@ -180,8 +180,8 @@ class TestBom(common.TransactionCase):
|
||||
# Total cost of Dining Table = (550) + Total cost of operations (125) = 675.0
|
||||
self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 675.0, "After computing price from BoM price should be 612.5")
|
||||
self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost()
|
||||
# Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 125) = 968.75
|
||||
self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 968.75, "After computing price from BoM price should be 786.46")
|
||||
# Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 10.42) = 854.17
|
||||
self.assertEquals(float_compare(self.dining_table.standard_price, 854.17, precision_digits=2), 0, "After computing price from BoM price should be 786.46")
|
||||
|
||||
def test_01_compute_price_inventory_valuation(self):
|
||||
"""Test update cost from bom in list view when inventory valuation is real time."""
|
||||
|
||||
@@ -28,7 +28,7 @@ class StripeController(http.Controller):
|
||||
try:
|
||||
acquirer.s2s_process(post)
|
||||
except Exception as e:
|
||||
error = e.message
|
||||
error = str(e)
|
||||
|
||||
return_url = post.get('return_url', '/')
|
||||
if error:
|
||||
|
||||
@@ -2170,6 +2170,19 @@ td {
|
||||
overflow: auto;
|
||||
height: 120px;
|
||||
}
|
||||
.pos .modal-dialog .popup-textinput{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pos .modal-dialog .popup-textinput .title,
|
||||
.pos .modal-dialog .popup-textinput .footer{
|
||||
flex: none;
|
||||
}
|
||||
.pos .modal-dialog .popup-textinput textarea{
|
||||
resize: none;
|
||||
flex: auto;
|
||||
margin: 0 20px 80px;
|
||||
}
|
||||
.pos .popup .title {
|
||||
background: rgba(255,255,255,0.5);
|
||||
margin: 0;
|
||||
|
||||
@@ -741,6 +741,9 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
|
||||
if(err.message === 'XmlHttpRequestError '){
|
||||
title = 'Network Failure (XmlHttpRequestError)';
|
||||
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
|
||||
}else if(err.message === 'TLSError'){
|
||||
title = 'Https connection to IoT Box failed';
|
||||
body = 'Make sure you are using IoT Box v18.10 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
|
||||
}else if(err.code === 200){
|
||||
title = err.data.message;
|
||||
body = err.data.debug;
|
||||
|
||||
@@ -192,11 +192,15 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
// try harder when we remember a good proxy url
|
||||
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
|
||||
.then(null,function(){
|
||||
return self.find_proxy(options);
|
||||
if (window.location.protocol != 'https:'){
|
||||
return self.find_proxy(options);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
// just find something quick
|
||||
found_url = this.find_proxy(options);
|
||||
if (window.location.protocol != 'https:'){
|
||||
found_url = this.find_proxy(options);
|
||||
}
|
||||
}
|
||||
|
||||
success = found_url.then(function(url){
|
||||
@@ -248,16 +252,17 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
// try several time to connect to a known proxy url
|
||||
try_hard_to_connect: function(url,options){
|
||||
options = options || {};
|
||||
var port = ':' + (options.port || '8069');
|
||||
var protocol = window.location.protocol;
|
||||
var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069');
|
||||
|
||||
this.set_connection_status('connecting');
|
||||
|
||||
if(url.indexOf('//') < 0){
|
||||
url = 'http://'+url;
|
||||
url = protocol + '//' + url;
|
||||
}
|
||||
|
||||
if(url.indexOf(':',5) < 0){
|
||||
url = url+port;
|
||||
url = url + port;
|
||||
}
|
||||
|
||||
// try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
|
||||
@@ -273,11 +278,11 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
.done(function(){
|
||||
done.resolve(url);
|
||||
})
|
||||
.fail(function(){
|
||||
.fail(function(resp){
|
||||
if(retries > 0){
|
||||
try_real_hard_to_connect(url,retries-1,done);
|
||||
}else{
|
||||
done.reject();
|
||||
done.reject(resp.statusText, url);
|
||||
}
|
||||
});
|
||||
return done;
|
||||
|
||||
@@ -127,13 +127,21 @@ exports.PosModel = Backbone.Model.extend({
|
||||
progress: function(prog){
|
||||
self.chrome.loading_progress(prog);
|
||||
},
|
||||
}).then(function(){
|
||||
if(self.config.iface_scan_via_proxy){
|
||||
self.barcode_reader.connect_to_proxy();
|
||||
}
|
||||
}).always(function(){
|
||||
done.resolve();
|
||||
});
|
||||
}).then(
|
||||
function(){
|
||||
if(self.config.iface_scan_via_proxy){
|
||||
self.barcode_reader.connect_to_proxy();
|
||||
}
|
||||
done.resolve();
|
||||
},
|
||||
function(statusText, url){
|
||||
if (statusText == 'error' && window.location.protocol == 'https:') {
|
||||
var error = {message: 'TLSError', url: url};
|
||||
self.chrome.loading_error(error);
|
||||
} else {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
return done;
|
||||
},
|
||||
|
||||
|
||||
@@ -1289,6 +1289,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
} else {
|
||||
fields.property_product_pricelist = false;
|
||||
}
|
||||
var contents = this.$(".client-details-contents");
|
||||
contents.off("click", ".button.save");
|
||||
|
||||
|
||||
rpc.query({
|
||||
model: 'res.partner',
|
||||
@@ -1308,6 +1311,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
'title': _t('Error: Could not Save Changes'),
|
||||
'body': error_body,
|
||||
});
|
||||
contents.on('click','.button.save',function(){ self.save_client_details(partner); });
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1325,6 +1329,8 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
// has created, and reload_partner() must have loaded the newly created partner.
|
||||
self.display_client_details('hide');
|
||||
}
|
||||
}).always(function(){
|
||||
$(".client-details-contents").on('click','.button.save',function(){ self.save_client_details(partner); });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -259,7 +259,8 @@ var TableWidget = PosBaseWidget.extend({
|
||||
args: [[['id', '=', table_id]], fields],
|
||||
limit: 1,
|
||||
})
|
||||
.then(function (table){
|
||||
.then(function (result){
|
||||
var table = result[0];
|
||||
for (var field in table) {
|
||||
self.table[field] = table[field];
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class PurchaseRequisition(models.Model):
|
||||
_order = "id desc"
|
||||
|
||||
def _get_picking_in(self):
|
||||
pick_in = self.env.ref('stock.picking_type_in')
|
||||
pick_in = self.env.ref('stock.picking_type_in', raise_if_not_found=False)
|
||||
company = self.env['res.company']._company_default_get('purchase.requisition')
|
||||
if not pick_in or pick_in.sudo().warehouse_id.company_id.id != company.id:
|
||||
pick_in = self.env['stock.picking.type'].search(
|
||||
|
||||
@@ -120,7 +120,7 @@ class AccountInvoice(models.Model):
|
||||
# valuation difference, it means this difference is due to exchange rates,
|
||||
# so we don't create anything, the exchange rate entries will
|
||||
# be processed automatically by the rest of the code.
|
||||
diff_res.append({
|
||||
diff_line = {
|
||||
'type': 'src',
|
||||
'name': i_line.name[:64],
|
||||
'price_unit': inv.currency_id.round(price_unit_val_dif),
|
||||
@@ -131,7 +131,11 @@ class AccountInvoice(models.Model):
|
||||
'uom_id': line['uom_id'],
|
||||
'account_analytic_id': line['account_analytic_id'],
|
||||
'tax_ids': tax_ids,
|
||||
})
|
||||
}
|
||||
# We update the original line accordingly
|
||||
line['price_unit'] = inv.currency_id.round(line['price_unit'] - diff_line['price_unit'])
|
||||
line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit'])
|
||||
diff_res.append(diff_line)
|
||||
return diff_res
|
||||
return []
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
})
|
||||
self.test_product_category.property_account_creditor_price_difference_categ = self.price_dif_account.id
|
||||
|
||||
def _create_purchase(self, product, quantity=1.0):
|
||||
def _create_purchase(self, product, date, quantity=1.0):
|
||||
rslt = self.env['purchase.order'].create({
|
||||
'partner_id': self.test_partner.id,
|
||||
'currency_id': self.currency_two.id,
|
||||
@@ -36,14 +36,14 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
'product_qty': quantity,
|
||||
'product_uom': product.uom_po_id.id,
|
||||
'price_unit': self.product_price_unit,
|
||||
'date_planned': '2017-10-07',
|
||||
'date_planned': date,
|
||||
})],
|
||||
'date_order': '2017-10-01',
|
||||
'date_order': date,
|
||||
})
|
||||
rslt.button_confirm()
|
||||
return rslt
|
||||
|
||||
def _create_invoice_for_po(self, purchase_order):
|
||||
def _create_invoice_for_po(self, purchase_order, date):
|
||||
account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
|
||||
rslt = self.env['account.invoice'].create({
|
||||
'purchase_id': purchase_order.id,
|
||||
@@ -51,8 +51,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
'currency_id': self.currency_two.id,
|
||||
'name': 'vendor bill',
|
||||
'type': 'in_invoice',
|
||||
'date_invoice': '2017-12-31',
|
||||
'date': '2017-12-31',
|
||||
'date_invoice': date,
|
||||
'date': date,
|
||||
'account_id': account_receivable.id,
|
||||
})
|
||||
rslt.purchase_order_change()
|
||||
@@ -62,11 +62,16 @@ 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)
|
||||
purchase_order = self._create_purchase(test_product, '2018-01-01')
|
||||
self._process_pickings(purchase_order.picking_ids)
|
||||
|
||||
invoice = self._create_invoice_for_po(purchase_order)
|
||||
self.currency_rate.rate = 7.76435463
|
||||
invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 7.76435463,
|
||||
'name': '2018-02-01',
|
||||
})
|
||||
invoice.action_invoice_open()
|
||||
picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
|
||||
self.check_reconciliation(invoice, picking)
|
||||
@@ -77,29 +82,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
def test_invoice_shipment(self):
|
||||
""" Tests the case into which we make the invoice first, and then receive the goods.
|
||||
"""
|
||||
# First, we delete the currency rate defined in demo data for USD on June 6th.
|
||||
# This is mandatory to ensure consistency of the data generated by the test,
|
||||
# as stock pickings created from a PO are by design always created for
|
||||
# the current date (there is no way forcing this), meaning that they
|
||||
# always use today's exchange rate for their valuation.
|
||||
# Despite the fact we rewrite the date of the valuation moves artificially,
|
||||
# we cannot correct the debit and credit values, since the anglosaxon
|
||||
# entries get automatically reconciled (and you cannot modify a reconciled entry).
|
||||
# So, we have to make sure that "today"'s rate will always be the last rate we
|
||||
# created in order to ensure complete control of the test.
|
||||
self.env.ref('base.rateUSDbis').unlink()
|
||||
|
||||
|
||||
# Create a PO and an invoice for it
|
||||
test_product = self.test_product_order
|
||||
purchase_order = self._create_purchase(test_product)
|
||||
purchase_order = self._create_purchase(test_product, '2017-12-01')
|
||||
|
||||
invoice = self._create_invoice_for_po(purchase_order)
|
||||
invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
|
||||
invoice_line.quantity = 1
|
||||
|
||||
# The currency rate changes
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 13.834739702,
|
||||
@@ -113,7 +105,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
self.check_reconciliation(invoice, picking)
|
||||
|
||||
# The currency rate changes again
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 10.54739702,
|
||||
@@ -132,7 +124,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
self._change_pickings_date(return_pick, '2018-01-13')
|
||||
|
||||
# The currency rate changes again
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 9.56564564,
|
||||
@@ -157,25 +149,40 @@ 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, quantity=5.0)
|
||||
purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
|
||||
self._process_pickings(purchase_order.picking_ids, quantity=2.0)
|
||||
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
|
||||
|
||||
invoice = self._create_invoice_for_po(purchase_order)
|
||||
invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
|
||||
invoice_line.quantity = 3
|
||||
self.currency_rate.rate = 7.76435463
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 7.76435463,
|
||||
'name': '2017-02-01',
|
||||
})
|
||||
invoice.action_invoice_open()
|
||||
self.check_reconciliation(invoice, picking, full_reconcile=False)
|
||||
|
||||
invoice2 = self._create_invoice_for_po(purchase_order)
|
||||
invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
|
||||
invoice_line.quantity = 2
|
||||
self.currency_rate.rate = 13.834739702
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 13.834739702,
|
||||
'name': '2017-03-01',
|
||||
})
|
||||
invoice2.action_invoice_open()
|
||||
self.check_reconciliation(invoice2, picking, full_reconcile=False)
|
||||
|
||||
self.currency_rate.rate = 12.195747002
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 12.195747002,
|
||||
'name': '2017-04-01',
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.stock.tests.common import TestStockCommon
|
||||
|
||||
|
||||
class TestReplenishWizard(TestStockCommon):
|
||||
def setUp(self):
|
||||
super(TestReplenishWizard, self).setUp()
|
||||
self.vendor = self.env['res.partner'].search([('supplier', '=', True)], limit=1)
|
||||
self.vendor = self.env['res.partner'].create(dict(name='The Replenisher', supplier=True))
|
||||
self.product1_price = 500
|
||||
|
||||
# Create a supplier info witch the previous vendor
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
@@ -417,3 +418,52 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
# check the anglo saxon entries
|
||||
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
|
||||
self.assertEqual(price_diff_entry.credit, 100)
|
||||
|
||||
def test_anglosaxon_valuation(self):
|
||||
self.env.user.company_id.anglo_saxon_accounting = True
|
||||
self.product1.product_tmpl_id.cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.valuation = 'real_time'
|
||||
self.product1.product_tmpl_id.invoice_policy = 'delivery'
|
||||
price_diff_account = self.env['account.account'].create({
|
||||
'name': 'price diff account',
|
||||
'code': 'price diff account',
|
||||
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
|
||||
})
|
||||
self.product1.property_account_creditor_price_difference = price_diff_account
|
||||
|
||||
# Create PO
|
||||
po_form = Form(self.env['purchase.order'])
|
||||
po_form.partner_id = self.partner_id
|
||||
with po_form.order_line.new() as po_line:
|
||||
po_line.product_id = self.product1
|
||||
po_line.product_qty = 1
|
||||
po_line.price_unit = 10.0
|
||||
order = po_form.save()
|
||||
order.button_confirm()
|
||||
|
||||
# Receive the goods
|
||||
receipt = order.picking_ids[0]
|
||||
receipt.move_lines.quantity_done = 1
|
||||
receipt.button_validate()
|
||||
|
||||
# Create an invoice with a different price
|
||||
invoice = self.env['account.invoice'].create({
|
||||
'partner_id': order.partner_id.id,
|
||||
'purchase_id': order.id,
|
||||
'account_id': order.partner_id.property_account_payable_id.id,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
invoice.purchase_order_change()
|
||||
invoice.invoice_line_ids[0].price_unit = 15.0
|
||||
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)])
|
||||
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)])
|
||||
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.")
|
||||
|
||||
@@ -39,6 +39,8 @@ KanbanRecord.include({
|
||||
.done(function() {
|
||||
self.trigger_up('kanban_record_update', {id: self.id});
|
||||
});
|
||||
// TODO: previous lines can be refactored as follows (in master)
|
||||
// self.trigger_up('kanban_record_update', {invoiced_target: value});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
odoo.define('sale.dashboard_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('Sales Team Dashboard', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'crm.team': {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: 'char'},
|
||||
invoiced_target: {string: "Invoiced_target", type: 'integer'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, foo: "yop"},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('edit target with several o_kanban_primary_bottom divs', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'crm.team',
|
||||
data: this.data,
|
||||
arch: '<kanban>' +
|
||||
'<templates>' +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div class="container o_kanban_card_content">' +
|
||||
'<a href="#" class="sales_team_target_definition o_inline_link">' +
|
||||
'Click to define a target</a>' +
|
||||
'<div class="col-12 o_kanban_primary_bottom"/>' +
|
||||
'<div class="col-12 o_kanban_primary_bottom bottom_block"/>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
assert.strictEqual(args.args[1].invoiced_target, 123,
|
||||
"new value is correctly saved");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_view .sales_team_target_definition').length, 1,
|
||||
"should have classname 'sales_team_target_definition'");
|
||||
assert.strictEqual(kanban.$('.o_kanban_primary_bottom').length, 2,
|
||||
"should have two divs with classname 'o_kanban_primary_bottom'");
|
||||
|
||||
kanban.$('a.sales_team_target_definition').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_primary_bottom:last input').length, 1,
|
||||
"should have rendered an input in the last o_kanban_primary_bottom div");
|
||||
|
||||
kanban.$('.o_kanban_primary_bottom:last input').focus();
|
||||
kanban.$('.o_kanban_primary_bottom:last input').val('123');
|
||||
kanban.$('.o_kanban_primary_bottom:last input').blur();
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -28,4 +28,10 @@
|
||||
<script type="text/javascript" src="/sale/static/tests/product_configurator.test.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="sales_team_dashboard_tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/sale/static/tests/sales_team_dashboard_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
#set the invoice_policy to delivery to have an accurate COGS entry
|
||||
self.test_product_delivery.invoice_policy = "delivery"
|
||||
|
||||
def _create_sale(self, product, quantity=1.0):
|
||||
def _create_sale(self, product, date, quantity=1.0):
|
||||
rslt = self.env['sale.order'].create({
|
||||
'partner_id': self.test_partner.id,
|
||||
'currency_id': self.currency_two.id,
|
||||
@@ -42,17 +42,18 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
'product_uom': product.uom_po_id.id,
|
||||
'price_unit': self.product_price_unit,
|
||||
})],
|
||||
'date_order': date,
|
||||
})
|
||||
rslt.action_confirm()
|
||||
return rslt
|
||||
|
||||
def _create_invoice_for_so(self, sale_order, product):
|
||||
def _create_invoice_for_so(self, sale_order, product, date):
|
||||
rslt = self.env['account.invoice'].create({
|
||||
'partner_id': self.test_partner.id,
|
||||
'currency_id': self.currency_two.id,
|
||||
'name': 'customer invoice',
|
||||
'type': 'out_invoice',
|
||||
'date_invoice': time.strftime('%Y') + '-12-22',
|
||||
'date_invoice': date,
|
||||
'account_id': self.account_receivable.id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'test line',
|
||||
@@ -92,11 +93,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
test_product = self.test_product_delivery
|
||||
self._set_initial_stock_for_product(test_product)
|
||||
|
||||
sale_order = self._create_sale(test_product)
|
||||
sale_order = self._create_sale(test_product, '2108-01-01')
|
||||
self._process_pickings(sale_order.picking_ids)
|
||||
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product)
|
||||
self.currency_rate.rate = 9.87366352
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12')
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 9.87366352,
|
||||
'name': '2018-02-01',
|
||||
})
|
||||
invoice.action_invoice_open()
|
||||
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
|
||||
self.check_reconciliation(invoice, picking, operation='sale')
|
||||
@@ -110,10 +116,15 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
self.test_product_delivery.standard_price = 13
|
||||
self._set_initial_stock_for_product(test_product)
|
||||
|
||||
sale_order = self._create_sale(test_product)
|
||||
sale_order = self._create_sale(test_product, '2018-01-01')
|
||||
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product)
|
||||
self.currency_rate.rate = 0.974784
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 0.974784,
|
||||
'name': '2018-02-01',
|
||||
})
|
||||
invoice.action_invoice_open()
|
||||
|
||||
self._process_pickings(sale_order.picking_ids)
|
||||
@@ -122,7 +133,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
self.check_reconciliation(invoice, picking, operation='sale')
|
||||
|
||||
#return the goods and refund the invoice
|
||||
self.currency_rate.rate = 10.54739702
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 10.54739702,
|
||||
'name': '2018-03-01',
|
||||
})
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=[picking.id], active_id=picking.id).create({})
|
||||
stock_return_picking.product_return_moves.quantity = 1.0
|
||||
@@ -131,7 +147,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
return_pick.action_assign()
|
||||
return_pick.move_lines.quantity_done = 1
|
||||
return_pick.action_done()
|
||||
self.currency_rate.rate = 9.56564564
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 9.56564564,
|
||||
'name': '2018-04-01',
|
||||
})
|
||||
refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({
|
||||
'description': 'test_invoice_shipment_refund',
|
||||
'filter_refund': 'cancel',
|
||||
@@ -147,26 +168,41 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
|
||||
test_product = self.test_product_delivery
|
||||
self._set_initial_stock_for_product(test_product)
|
||||
|
||||
sale_order = self._create_sale(test_product, quantity=5)
|
||||
sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
|
||||
|
||||
self._process_pickings(sale_order.picking_ids, quantity=2.0)
|
||||
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1)
|
||||
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product)
|
||||
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
|
||||
invoice_line.quantity = 3
|
||||
self.currency_rate.rate = 7.76435463
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 7.76435463,
|
||||
'name': '2018-02-01',
|
||||
})
|
||||
invoice.action_invoice_open()
|
||||
self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale')
|
||||
|
||||
invoice2 = self._create_invoice_for_so(sale_order, test_product)
|
||||
invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12')
|
||||
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
|
||||
invoice_line.quantity = 2
|
||||
self.currency_rate.rate = 13.834739702
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 13.834739702,
|
||||
'name': '2018-03-01',
|
||||
})
|
||||
invoice2.action_invoice_open()
|
||||
self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale')
|
||||
|
||||
self.currency_rate.rate = 12.195747002
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 12.195747002,
|
||||
'name': '2018-04-01',
|
||||
})
|
||||
self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
|
||||
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1)
|
||||
self.check_reconciliation(invoice2, picking, operation='sale')
|
||||
|
||||
@@ -361,8 +361,10 @@ class ProcurementGroup(models.Model):
|
||||
self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id)
|
||||
|
||||
# Search all confirmed stock_moves and try to assign them
|
||||
confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc')
|
||||
for moves_chunk in split_every(100, confirmed_moves.ids):
|
||||
moves_to_assign = self.env['stock.move'].search([
|
||||
('state', 'in', ['confirmed', 'partially_available']), ('product_uom_qty', '!=', 0.0)
|
||||
], limit=None, order='priority desc, date_expected asc')
|
||||
for moves_chunk in split_every(100, moves_to_assign.ids):
|
||||
self.env['stock.move'].browse(moves_chunk)._action_assign()
|
||||
if use_new_cursor:
|
||||
self._cr.commit()
|
||||
|
||||
@@ -332,9 +332,11 @@
|
||||
<field name="group_id" groups="base.group_no_one"/>
|
||||
<field name="priority" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
|
||||
</group>
|
||||
<field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Note" name="note">
|
||||
<field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -58,12 +58,6 @@ class ValuationReconciliationTestCase(AccountingTestCase):
|
||||
self.currency_one = self.company.currency_id
|
||||
currency_two_name = 'USD' if self.currency_one.name != 'USD' else 'EUR'
|
||||
self.currency_two = self.env['res.currency'].search([('name', '=', currency_two_name)])
|
||||
self.currency_rate = self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_one.id,
|
||||
'company_id': self.company.id,
|
||||
'rate': 1.234343354, # Totally arbitratry value
|
||||
'name': '2017-01-01',
|
||||
})
|
||||
|
||||
self.input_account = self.env['account.account'].create({
|
||||
'name': 'Test stock in',
|
||||
@@ -127,3 +121,15 @@ class ValuationReconciliationTestCase(AccountingTestCase):
|
||||
})
|
||||
|
||||
self.product_price_unit = 66.0
|
||||
|
||||
# We delete the currency rate defined in demo data for USD on June 6th.
|
||||
# This is mandatory to ensure consistency of the data generated by the test,
|
||||
# as stock pickings created from a PO are by design always created for
|
||||
# the current date (there is no way forcing this), meaning that they
|
||||
# always use today's exchange rate for their valuation.
|
||||
# Despite the fact we rewrite the date of the valuation moves artificially,
|
||||
# we cannot correct the debit and credit values, since the anglosaxon
|
||||
# entries get automatically reconciled (and you cannot modify a reconciled entry).
|
||||
# So, we have to make sure that "today"'s rate will always be the last rate we
|
||||
# created in order to ensure complete control of the test.
|
||||
self.env.ref('base.rateUSDbis').unlink()
|
||||
|
||||
@@ -2188,7 +2188,7 @@ var BasicModel = AbstractModel.extend({
|
||||
* @param {Object} [options]
|
||||
* @param {string[]} [options.fieldNames] the list of fields to fetch. If
|
||||
* not given, fetch all the fields in record.fieldNames (+ display_name)
|
||||
* @param {string} [optinos.viewType] the type of view for which the record
|
||||
* @param {string} [options.viewType] the type of view for which the record
|
||||
* is fetched (usefull to load the adequate fields), by defaults, uses
|
||||
* record.viewType
|
||||
* @returns {Deferred<Object>} resolves to the record or is rejected in
|
||||
@@ -3785,11 +3785,12 @@ var BasicModel = AbstractModel.extend({
|
||||
*/
|
||||
_postprocess: function (record, options) {
|
||||
var self = this;
|
||||
var viewType = options && options.viewType || record.viewType;
|
||||
var defs = [];
|
||||
|
||||
_.each(record.getFieldNames(options), function (name) {
|
||||
var field = record.fields[name];
|
||||
var fieldInfo = record.fieldsInfo[record.viewType][name] || {};
|
||||
var fieldInfo = record.fieldsInfo[viewType][name] || {};
|
||||
var options = fieldInfo.options || {};
|
||||
if (options.always_reload) {
|
||||
if (record.fields[name].type === 'many2one' && record.data[name]) {
|
||||
@@ -3798,7 +3799,7 @@ var BasicModel = AbstractModel.extend({
|
||||
model: field.relation,
|
||||
method: 'name_get',
|
||||
args: [element.data.id],
|
||||
context: self._getContext(record, {fieldName: name}),
|
||||
context: self._getContext(record, {fieldName: name, viewType: viewType}),
|
||||
})
|
||||
.then(function (result) {
|
||||
element.data.display_name = result[0][1];
|
||||
|
||||
@@ -150,6 +150,14 @@ var BasicView = AbstractView.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
// Many2one: context is not the same between the different views
|
||||
// this means the result of a name_get could differ
|
||||
if (fieldType === 'many2one') {
|
||||
if (JSON.stringify(record.data[name].context) !==
|
||||
JSON.stringify(fieldInfo.context)) {
|
||||
fieldNames.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ var KanbanController = BasicController.extend({
|
||||
_onButtonNew: function () {
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
|
||||
if (this.on_create === 'quick_create' && quickCreateEnabled) {
|
||||
if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) {
|
||||
// Activate the quick create in the first column
|
||||
this.renderer.addQuickCreate();
|
||||
} else if (this.on_create && this.on_create !== 'quick_create') {
|
||||
|
||||
@@ -2332,7 +2332,7 @@ QUnit.module('basic_fields', {
|
||||
var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
|
||||
evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
|
||||
$('.discreteBar')[0].dispatchEvent(evt);
|
||||
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText;
|
||||
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText.trim();
|
||||
assert.equal(tooltip, graph_key, "graph tooltip should be generated ");
|
||||
$('.nvtooltip').remove();
|
||||
|
||||
|
||||
@@ -380,6 +380,93 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('show_address works in a view embedded in a view of another type', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.records[1].turtle_trululu = 2;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtles"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
"turtle,false,form": '<form string="T">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||
'</form>',
|
||||
"turtle,false,list": '<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_get') {
|
||||
return this._super(route, args).then(function (result) {
|
||||
if (args.model === 'partner' && args.kwargs.context.show_address) {
|
||||
result[0][1] += '\nrue morgue\nparis 75013';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
// click the turtle field, opens a modal with the turtle form view
|
||||
form.$('.o_data_row:first td.o_data_cell').click();
|
||||
|
||||
assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013",
|
||||
"The partner's address should be displayed");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2one data is reloaded if there is a context to take into account', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.records[1].turtle_trululu = 2;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtles"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
"turtle,false,form": '<form string="T">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||
'</form>',
|
||||
"turtle,false,list": '<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu"/>' +
|
||||
'</tree>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_get') {
|
||||
return this._super(route, args).then(function (result) {
|
||||
if (args.model === 'partner' && args.kwargs.context.show_address) {
|
||||
result[0][1] += '\nrue morgue\nparis 75013';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
// click the turtle field, opens a modal with the turtle form view
|
||||
form.$('.o_data_row:first').click();
|
||||
|
||||
assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013",
|
||||
"The partner's address should be displayed");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2ones in form views with search more', function (assert) {
|
||||
assert.expect(3);
|
||||
this.data.partner.records.push({
|
||||
|
||||
@@ -4737,6 +4737,30 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick_create on grouped kanban without column', function (assert) {
|
||||
assert.expect(1);
|
||||
this.data.partner.records = [];
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create"><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['product_id'],
|
||||
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
assert.ok(true, "switch_view was called instead of quick_create");
|
||||
},
|
||||
},
|
||||
});
|
||||
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
|
||||
@@ -611,6 +611,14 @@ var RTEWidget = Widget.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
// Removes strange _moz_abspos attribute when it appears. Cannot
|
||||
// find another solution which works in all cases. A grabber still
|
||||
// appears at the same time which I did not manage to remove.
|
||||
// TODO find a complete and better solution
|
||||
_.defer(function () {
|
||||
$editable.find('[_moz_abspos]').removeAttr('_moz_abspos');
|
||||
});
|
||||
|
||||
if ($target.is('a')) {
|
||||
/**
|
||||
* Remove content editable everywhere and add it on the link only so that characters can be added
|
||||
|
||||
@@ -280,15 +280,36 @@ var SnippetOption = Widget.extend({
|
||||
*/
|
||||
_setActive: function () {
|
||||
var self = this;
|
||||
this.$el.find('[data-toggle-class], [data-select-class]')
|
||||
.addBack('[data-toggle-class], [data-select-class]')
|
||||
this.$el.find('[data-toggle-class]')
|
||||
.addBack('[data-toggle-class]')
|
||||
.removeClass('active')
|
||||
.filter(function () {
|
||||
var $elem = $(this);
|
||||
var className = $elem.data('toggleClass') || $elem.data('selectClass');
|
||||
return self.$target.hasClass(className);
|
||||
var className = $(this).data('toggleClass');
|
||||
return !className || self.$target.hasClass(className);
|
||||
})
|
||||
.addClass('active');
|
||||
|
||||
_processSelectClassElements(this.$el);
|
||||
_.each(this.$el.find('.dropdown-menu'), function (group) {
|
||||
_processSelectClassElements($(group).children());
|
||||
});
|
||||
|
||||
function _processSelectClassElements($elements) {
|
||||
var maxNbClasses = -1;
|
||||
$elements.filter('[data-select-class]')
|
||||
.removeClass('active')
|
||||
.filter(function () {
|
||||
var className = $(this).data('selectClass');
|
||||
var nbClasses = className ? className.split(' ').length : 0;
|
||||
if (nbClasses >= maxNbClasses && (!className || self.$target.hasClass(className))) {
|
||||
maxNbClasses = nbClasses;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.last()
|
||||
.addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -749,7 +770,7 @@ registry.colorpicker = SnippetOption.extend({
|
||||
*/
|
||||
_onColorResetButtonClick: function () {
|
||||
this.$target.removeClass(this.classes).css('background-color', '');
|
||||
self.$target.trigger('content_changed');
|
||||
this.$target.trigger('content_changed');
|
||||
this.$el.find('.colorpicker button.selected').removeClass('selected');
|
||||
},
|
||||
});
|
||||
@@ -852,7 +873,9 @@ registry.background = SnippetOption.extend({
|
||||
*/
|
||||
setTarget: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// TODO should be automatic for all options as equal to the start method
|
||||
this.bindBackgroundEvents();
|
||||
this.__customImageSrc = this._getSrcFromCssValue();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -111,6 +111,11 @@ class Website(models.Model):
|
||||
('b2c', 'Free sign up'),
|
||||
], string='Customer Account', default='b2b')
|
||||
|
||||
@api.onchange('language_ids')
|
||||
def _onchange_language_ids(self):
|
||||
if self.language_ids and self.default_lang_id not in self.language_ids:
|
||||
self.default_lang_id = self.language_ids[0]
|
||||
|
||||
@api.multi
|
||||
def _compute_menu(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
@@ -271,6 +271,7 @@ options.registry.carousel = options.Class.extend({
|
||||
* @override
|
||||
*/
|
||||
_setActive: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$el.find('[data-interval]').removeClass('active')
|
||||
.filter('[data-interval=' + this.$target.attr('data-interval') + ']').addClass('active');
|
||||
},
|
||||
|
||||
@@ -392,11 +392,16 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
|
||||
@http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False)
|
||||
def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
|
||||
sale_order = request.website.sale_get_order(force_create=1)
|
||||
if sale_order.state != 'draft':
|
||||
request.session['sale_order_id'] = None
|
||||
sale_order = request.website.sale_get_order(force_create=True)
|
||||
|
||||
product_custom_attribute_values = None
|
||||
if kw.get('product_custom_attribute_values'):
|
||||
product_custom_attribute_values = json.loads(kw.get('product_custom_attribute_values'))
|
||||
|
||||
request.website.sale_get_order(force_create=1)._cart_update(
|
||||
sale_order._cart_update(
|
||||
product_id=int(product_id),
|
||||
add_qty=add_qty,
|
||||
set_qty=set_qty,
|
||||
@@ -569,6 +574,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['website_id'] = request.website.id
|
||||
|
||||
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
|
||||
|
||||
@@ -44,6 +44,8 @@ def tokenize(typespec):
|
||||
yield (LITERAL, ast.literal_eval(string))
|
||||
elif toktype == token.ENDMARKER:
|
||||
return
|
||||
elif toktype == token.NEWLINE:
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Unknown typespec token %s" % token.tok_name[toktype])
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
|
||||
else:
|
||||
opts = {}
|
||||
|
||||
def warner(msg):
|
||||
def warner(msg, **kw):
|
||||
self.builder.warn(msg, (self.builder.current_docname, node.line))
|
||||
highlighted = self.builder.highlighter.highlight_block(
|
||||
node.rawsource, lang, opts=opts, warn=warner, linenos=linenos,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
The Netherlands, 2018-10-24
|
||||
|
||||
Brahoo B.V. agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers
|
||||
|
||||
List of contributors:
|
||||
|
||||
Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers
|
||||
@@ -0,0 +1,12 @@
|
||||
India, 17, Sept - 2018
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Anil Kesariya anil.r.kesariya@gmail.com https://github.com/AnilKesariya
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Mexico, 2018-10-24
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Luis Guzman ark@switnet.org https://github.com/ark74
|
||||
@@ -0,0 +1,11 @@
|
||||
Argentina, 2018-10-18
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Marcelo Ariel Hamra marcelo.hamra@gmail.com https://github.com/mhamra
|
||||
@@ -1010,17 +1010,17 @@ them (e.g. to change their default sort order):
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/extension.py
|
||||
:language: python
|
||||
:lines: 5-
|
||||
:lines: 7-
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py
|
||||
:language: python
|
||||
:lines: 8,13
|
||||
:lines: 10,15
|
||||
|
||||
will yield:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py
|
||||
:language: text
|
||||
:lines: 11
|
||||
:lines: 13
|
||||
|
||||
.. note:: it will also yield the various :ref:`automatic fields
|
||||
<reference/orm/model/automatic>` unless they've been disabled
|
||||
|
||||
@@ -81,8 +81,11 @@ Overview
|
||||
|
||||
The Credits
|
||||
|
||||
.. note:: The credits went from integer to float value starting **October 2018**
|
||||
Integer values are still supported.
|
||||
|
||||
Every service provided through the In-App platform can be used by the
|
||||
clients with tokens or *credits*. The credits are an integer unit and
|
||||
clients with tokens or *credits*. The credits are an float unit and
|
||||
their monetary value depends on the service and is decided by the
|
||||
provider. This could be:
|
||||
|
||||
@@ -157,7 +160,8 @@ The first step is to register your service on the IAP endpoint (production
|
||||
and/or test) before you can actually query user accounts. To create a service,
|
||||
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
|
||||
production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
||||
*independent* and *not synchronized*).
|
||||
*independent* and *not synchronized*). Alternatively, you can go to your portal
|
||||
on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -166,33 +170,45 @@ production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
||||
on sandbox to ease the tests.
|
||||
|
||||
Log in then go to :menuselection:`My Account --> Your In-App Services`, click
|
||||
Create and provide the name of your service.
|
||||
Create and provide the informations of your service.
|
||||
|
||||
|
||||
The now created service has *two* important fields:
|
||||
The service has *four* important fields:
|
||||
|
||||
* :samp:`name` - :class:`ServiceName`: this will identify your service in the
|
||||
client's :ref:`app <iap-odoo-app>` communicates directly with IAP.
|
||||
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
|
||||
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
|
||||
the client's account.
|
||||
|
||||
client's :ref:`app <iap-odoo-app>` communicates directly with IAP, choose it
|
||||
carefully!
|
||||
.. warning::
|
||||
The :class:`ServiceName` is unique and should usually match the name of your
|
||||
Odoo App.
|
||||
Odoo Client App.
|
||||
|
||||
* :samp:`Icon` - :class:`Icon`: A generic icon that will serve as default for your
|
||||
:ref:`packages <iap-packages>`
|
||||
|
||||
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
|
||||
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
|
||||
the client's account. It will be shown only once upon creation of the service
|
||||
and can be regenerated at will.
|
||||
.. danger::
|
||||
Your :class:`ServiceKey` *is a secret*, leaking your service key
|
||||
allows other application developers to draw credits bought for
|
||||
your service(s).
|
||||
|
||||
.. image:: images/service_select.png
|
||||
* :samp:`privacy policy` - :class:`PrivacyPolicy`: This is an url to the privacy
|
||||
policy of your service. This should explicitly mention the **information you collect**,
|
||||
How you **use it, its relevance** to make your service work and inform the
|
||||
client on how they can **access, update or delete their personal information**.
|
||||
|
||||
.. image:: images/menu.png
|
||||
:align: center
|
||||
|
||||
.. image:: images/service_create.png
|
||||
.. image:: images/service_list.png
|
||||
:align: center
|
||||
|
||||
.. image:: images/service_packs.png
|
||||
.. image:: images/creating_service.png
|
||||
:align: center
|
||||
|
||||
.. image:: images/service_created.png
|
||||
:align: center
|
||||
|
||||
You can then create *credit packs* which clients can purchase in order to
|
||||
@@ -203,9 +219,10 @@ use your service.
|
||||
Packages
|
||||
--------
|
||||
|
||||
The credit packages are essentially a product with 4 characteristics.
|
||||
The credit packages are essentially a product with 5 characteristics.
|
||||
|
||||
* Name: the name of the package,
|
||||
* Icon: A specific icon for the package. If not provided, it will fallback on the service icon
|
||||
* Description: details on the package that will appear on the shop page as
|
||||
well as the invoice,
|
||||
* Credits: the amount of credits the client is entitled to when buying the package,
|
||||
@@ -362,7 +379,6 @@ parameters we can use to make things clearer to the end-user:
|
||||
.. patch::
|
||||
|
||||
|
||||
|
||||
.. TODO:: how do you test your service?
|
||||
|
||||
JSON-RPC2_ Transaction API
|
||||
@@ -394,13 +410,13 @@ Authorize
|
||||
|
||||
:param ServiceKey key:
|
||||
:param UserToken account_token:
|
||||
:param int credit:
|
||||
:param float credit:
|
||||
:param str description: optional, helps users identify the reason for
|
||||
charges on their accounts.
|
||||
:returns: :class:`TransactionToken` if the authorization succeeded.
|
||||
:raises: :class:`~odoo.exceptions.AccessError` if the service token is invalid
|
||||
:raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does
|
||||
:raises: ``TypeError`` if the ``credit`` value is not an integer
|
||||
:raises: ``TypeError`` if the ``credit`` value is not an integer or a float
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -434,7 +450,7 @@ Capture
|
||||
|
||||
:param TransactionToken token:
|
||||
:param ServiceKey key:
|
||||
:param int credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
|
||||
:param float credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
|
||||
:raises: :class:`~odoo.exceptions.AccessError`
|
||||
|
||||
.. code-block:: python
|
||||
@@ -534,6 +550,33 @@ care how they are implemented
|
||||
|
||||
Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
|
||||
|
||||
|
||||
Test the API
|
||||
------------
|
||||
|
||||
In order to test the developped app, we propose a sandbox platform that allows you to:
|
||||
|
||||
1. Test the whole flow from the client's point of view - Actual services and transactions
|
||||
that can be consulted. (again this requires to change the endpoint, see the danger note
|
||||
in :ref:`Service <iap-service>`)
|
||||
2. Test the API.
|
||||
|
||||
The latter consists in specific tokens that will work on **IAP-Sandbox only**.
|
||||
|
||||
* token ``000000``: represents a non-existing account. Returns
|
||||
an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
|
||||
* token ``000111``: Represents an account without sufficient credits to perform any service.
|
||||
Returns an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
|
||||
* token ``111111``: Represents an account with enough credits to perform any service.
|
||||
An authorize attempt will return a dummy transacion token that is processed by the capture
|
||||
and cancel routes.
|
||||
|
||||
.. note::
|
||||
|
||||
* Those tokens are only active on the IAP-Sanbox server.
|
||||
* The service key is completely ignored with this flow, If you want to run a robust test
|
||||
of your service, you should ignore these tokens.
|
||||
|
||||
Odoo Helpers
|
||||
============
|
||||
|
||||
@@ -548,7 +591,7 @@ Charging
|
||||
.. note::
|
||||
|
||||
A new functionality was introduced to capture a different amount of credits than reserved.
|
||||
As this patch was added on the 15th of January 2018, you will need to upgrade your ``iap`` module in order to use it.
|
||||
As this patch was added on the **15th of January 2018**, you will need to upgrade your ``iap`` module in order to use it.
|
||||
The specifics of the new functionality are highlighted in the code.
|
||||
|
||||
.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template])
|
||||
@@ -567,12 +610,12 @@ Charging
|
||||
configuration key
|
||||
:param ServiceKey key:
|
||||
:param UserToken token:
|
||||
:param int credit:
|
||||
:param float credit:
|
||||
:param str description:
|
||||
:param Qweb template credit_template:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 10,13,14,15
|
||||
:emphasize-lines: 11,13,14,15
|
||||
|
||||
@route('/deathstar/superlaser', type='json')
|
||||
def superlaser(self, user_account,
|
||||
@@ -583,7 +626,8 @@ Charging
|
||||
0.0 is none, 1.0 is full power
|
||||
"""
|
||||
credits = int(MAXIMUM_POWER * factor)
|
||||
with charge(request.env, SERVICE_KEY, user_account, credits) as transaction:
|
||||
description = "We will demonstrate the power of this station on your home planet of Alderaan."
|
||||
with charge(request.env, SERVICE_KEY, user_account, credits, description) as transaction:
|
||||
# TODO: allow other targets
|
||||
transaction.credit = max(credits, 2)
|
||||
# Sales ongoing one the energy price,
|
||||
|
||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -61,7 +61,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",
|
||||
"base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1"
|
||||
"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True","7"
|
||||
"base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True","1"
|
||||
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","1"
|
||||
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S %p","True","1"
|
||||
"base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1"
|
||||
"base.lang_es_GT","Spanish (GT) / Español (GT)","es_GT","es_GT","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7"
|
||||
"base.lang_es_MX","Spanish (MX) / Español (MX)","es_MX","es_MX","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7"
|
||||
|
||||
|
@@ -670,8 +670,9 @@
|
||||
<field name="symbol">RD$</field>
|
||||
<field name="rounding">0.01</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="currency_unit_label">Peso</field>
|
||||
<field name="currency_unit_label">Pesos</field>
|
||||
<field name="currency_subunit_label">Centavos</field>
|
||||
<field name="position">before</field>
|
||||
</record>
|
||||
|
||||
<record id="ECS" model="res.currency">
|
||||
|
||||
@@ -695,11 +695,10 @@ class Module(models.Model):
|
||||
mod.write(updated_values)
|
||||
else:
|
||||
mod_path = modules.get_module_path(mod_name)
|
||||
if not mod_path:
|
||||
if not mod_path or not terp:
|
||||
continue
|
||||
if not terp or not terp.get('installable', True):
|
||||
continue
|
||||
mod = self.create(dict(name=mod_name, state='uninstalled', **values))
|
||||
state = "uninstalled" if terp.get('installable', True) else "uninstallable"
|
||||
mod = self.create(dict(name=mod_name, state=state, **values))
|
||||
res[1] += 1
|
||||
|
||||
mod._update_dependencies(terp.get('depends', []))
|
||||
|
||||
@@ -387,8 +387,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
||||
def copy(self, values):
|
||||
raise UserError(_("Cannot duplicate configuration!"), "")
|
||||
|
||||
# TODO: Find replacement for 'onchange' attribute in view with dynamic
|
||||
# api.onchange(...) and migrate the onchange_module(...) accordingly.
|
||||
@api.model
|
||||
def fields_view_get(self, view_id=None, view_type='form',
|
||||
toolbar=False, submenu=False):
|
||||
@@ -410,9 +408,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
||||
modifiers = json.loads(node.get("modifiers"))
|
||||
modifiers['readonly'] = True
|
||||
node.set("modifiers", json.dumps(modifiers))
|
||||
if 'on_change' not in node.attrib:
|
||||
node.set("on_change",
|
||||
"onchange_module(%s, '%s')" % (field, field))
|
||||
|
||||
ret_val['arch'] = etree.tostring(doc, encoding='unicode')
|
||||
return ret_val
|
||||
@@ -436,6 +431,16 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
||||
}
|
||||
return {}
|
||||
|
||||
def _register_hook(self):
|
||||
""" Add an onchange method for each module field. """
|
||||
def make_method(name):
|
||||
return lambda self: self.onchange_module(self[name], name)
|
||||
|
||||
for name in self._fields:
|
||||
if name.startswith('module_'):
|
||||
method = make_method(name)
|
||||
self._onchange_methods[name].append(method)
|
||||
|
||||
@api.model
|
||||
def _get_classified_fields(self):
|
||||
""" return a dictionary with the fields classified by category::
|
||||
|
||||
@@ -2822,7 +2822,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
record._cache.update(record._convert_to_cache(values, validate=False))
|
||||
if not self.env.cache.contains(self, field):
|
||||
exc = AccessError("No value found for %s.%s" % (self, field.name))
|
||||
self.env.cache.set_failed(self, field, exc)
|
||||
self.env.cache.set_failed(self, [field], exc)
|
||||
|
||||
@api.multi
|
||||
def _read_from_database(self, field_names, inherited_field_names=[]):
|
||||
|
||||
@@ -56,8 +56,8 @@ class MigrationManager(object):
|
||||
function. Theses files must respect a directory tree structure: A 'migrations' folder
|
||||
which containt a folder by version. Version can be 'module' version or 'server.module'
|
||||
version (in this case, the files will only be processed by this version of the server).
|
||||
Python file names must start by `pre` or `post` and will be executed, respectively,
|
||||
before and after the module initialisation. `end` scripts are run after all modules have
|
||||
Python file names must start by `pre-` or `post-` and will be executed, respectively,
|
||||
before and after the module initialisation. `end-` scripts are run after all modules have
|
||||
been updated.
|
||||
Example:
|
||||
<moduledir>
|
||||
|
||||
@@ -530,7 +530,8 @@ class ChromeBrowser():
|
||||
'--user-data-dir': self.user_data_dir,
|
||||
'--disable-translate': '',
|
||||
'--window-size': '1366x768',
|
||||
'--remote-debugging-port': str(self.devtools_port)
|
||||
'--remote-debugging-port': str(self.devtools_port),
|
||||
'--no-sandbox': '',
|
||||
}
|
||||
cmd = [self.executable]
|
||||
cmd += ['%s=%s' % (k, v) if v else k for k, v in switches.items()]
|
||||
|
||||
@@ -33,7 +33,7 @@ pytz==2016.7
|
||||
pyusb==1.0.0
|
||||
qrcode==5.3
|
||||
reportlab==3.3.0
|
||||
requests==2.11.1
|
||||
requests==2.20.0
|
||||
suds-jurko==0.6
|
||||
vatnumber==1.2
|
||||
vobject==0.9.3
|
||||
|
||||
@@ -27,7 +27,7 @@ pytz==2016.7
|
||||
pyusb==1.0.0
|
||||
qrcode==5.3
|
||||
reportlab>=3.3.0
|
||||
requests==2.11.1
|
||||
requests==2.20.0
|
||||
six==1.10.0
|
||||
suds-jurko==0.6
|
||||
vatnumber==1.2
|
||||
|
||||