[MERGE] forward port branch saas-11.1 up to f6ef0c7760

This commit is contained in:
Christophe Simonis
2018-01-10 20:35:53 +01:00
81 changed files with 2785 additions and 1777 deletions
+16 -7
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-13 14:36+0000\n"
"PO-Revision-Date: 2017-12-13 14:36+0000\n"
"POT-Creation-Date: 2018-01-10 07:40+0000\n"
"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -701,11 +701,6 @@ msgstr ""
msgid "Account"
msgstr ""
#. module: account
#: model:ir.model.fields,field_description:account.field_res_config_settings_module_account_accountant
msgid "Account Accountant"
msgstr ""
#. module: account
#: model:ir.model,name:account.model_account_aged_trial_balance
msgid "Account Aged Trial balance Report"
@@ -1024,6 +1019,7 @@ msgstr ""
#. module: account
#: model:ir.ui.menu,name:account.account_account_menu
#: model:ir.ui.view,arch_db:account.product_template_form_view
#: model:ir.model.fields,field_description:account.field_res_config_settings_module_account_accountant
msgid "Accounting"
msgstr ""
@@ -3276,6 +3272,19 @@ msgstr ""
msgid "Deactivate the \"Configuration Steps\" bar on the dashboard"
msgstr ""
#. module: account
#: code:addons/account/models/company.py:43
#, python-format
msgid "Dear Sir/Madam,\n"
"\n"
"Our records indicate that some payments on your account are still due. Please find details below.\n"
"If the amount has already been paid, please disregard this notice. Otherwise, please forward us the total amount stated below.\n"
"If you have any queries regarding your account, Please contact us.\n"
"\n"
"Thank you in advance for your cooperation.\n"
"Best Regards,"
msgstr ""
#. module: account
#: model:ir.model.fields,field_description:account.field_account_move_line_debit
#: model:ir.ui.view,arch_db:account.report_financial
+4 -1
View File
@@ -290,7 +290,8 @@ class AccountChartTemplate(models.Model):
tmp1, tmp2 = self.parent_id._install_template(company, code_digits=code_digits, transfer_account_id=transfer_account_id, acc_ref=acc_ref, taxes_ref=taxes_ref)
acc_ref.update(tmp1)
taxes_ref.update(tmp2)
tmp1, tmp2 = self._load_template(company, code_digits=code_digits, transfer_account_id=transfer_account_id, account_ref=acc_ref, taxes_ref=taxes_ref)
# Ensure, even if individually, that everything is translated according to the company's language.
tmp1, tmp2 = self.with_context(lang=company.partner_id.lang)._load_template(company, code_digits=code_digits, transfer_account_id=transfer_account_id, account_ref=acc_ref, taxes_ref=taxes_ref)
acc_ref.update(tmp1)
taxes_ref.update(tmp2)
return acc_ref, taxes_ref
@@ -854,6 +855,8 @@ class WizardMultiChartsAccounts(models.TransientModel):
all the provided information to create the accounts, the banks, the journals, the taxes, the
accounting properties... accordingly for the chosen company.
'''
# Ensure everything is translated consitingly to the company's language, not the user's one.
self = self.with_context(lang=self.company_id.partner_id.lang)
if not self.env.user._is_admin():
raise AccessError(_("Only administrators can change the settings"))
+5 -5
View File
@@ -41,14 +41,14 @@ class ResCompany(models.Model):
property_stock_valuation_account_id = fields.Many2one('account.account', string="Account Template for Stock Valuation")
bank_journal_ids = fields.One2many('account.journal', 'company_id', domain=[('type', '=', 'bank')], string='Bank Journals')
overdue_msg = fields.Text(string='Overdue Payments Message', translate=True,
default='''Dear Sir/Madam,
default=lambda s: _('''Dear Sir/Madam,
Our records indicate that some payments on your account are still due. Please find details below.
If the amount has already been paid, please disregard this notice. Otherwise, please forward us the total amount stated below.
If you have any queries regarding your account, Please contact us.
Thank you in advance for your cooperation.
Best Regards,''')
Best Regards,'''))
tax_exigibility = fields.Boolean(string='Use Cash Basis')
#Fields of the setup step for opening move
@@ -292,14 +292,14 @@ Best Regards,''')
unaffected_earnings_type = self.env.ref("account.data_unaffected_earnings")
account = self.env['account.account'].search([('company_id', '=', self.id),
('user_type_id', '=', unaffected_earnings_type.id)])
if not account:
account = self.env['account.account'].create({
if account:
return account[0]
return self.env['account.account'].create({
'code': '999999',
'name': _('Undistributed Profits/Losses'),
'user_type_id': unaffected_earnings_type.id,
'company_id': self.id,
})
return account
def get_opening_move_differences(self, opening_move_lines):
currency = self.currency_id
@@ -23,6 +23,10 @@
<field name="groups_id" eval="[(4, ref('account.group_account_invoice')), (4, ref('account.group_account_manager'))]"/>
</record>
<record id="base.group_system" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_account_manager'))]"/>
</record>
<record id="group_warning_account" model="res.groups">
<field name="name">A warning can be set on a partner (Account)</field>
<field name="category_id" ref="base.module_category_hidden"/>
+2 -2
View File
@@ -105,10 +105,10 @@ class StockPicking(models.Model):
picking.weight = sum(move.weight for move in picking.move_lines if move.state != 'cancel')
@api.multi
def do_transfer(self):
def action_done(self):
# TDE FIXME: should work in batch
self.ensure_one()
res = super(StockPicking, self).do_transfer()
res = super(StockPicking, self).action_done()
if self.carrier_id and self.carrier_id.integration_level == 'rate_and_ship':
self.send_to_shipper()
+6
View File
@@ -360,6 +360,12 @@ msgstr ""
msgid "${object.event_id.name}: ${object.get_date_range_str()}"
msgstr ""
#. module: event
#: code:addons/event/models/event.py:297
#, python-format
msgid "%s (copy)"
msgstr ""
#. module: event
#: model:ir.ui.view,arch_db:event.view_event_kanban
msgid "<i class=\"fa fa-clock-o\"/>\n"
+6
View File
@@ -291,6 +291,12 @@ class EventEvent(models.Model):
self.message_subscribe([vals['organizer_id']])
return res
@api.multi
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, name=_("%s (copy)") % (self.name))
return super(EventEvent, self).copy(default)
@api.one
def button_draft(self):
self.state = 'draft'
@@ -73,8 +73,8 @@ class HrHolidaySummaryReport(models.AbstractModel):
for index in range(0, ((date_to - date_from).days + 1)):
if date_from >= start_date and date_from <= end_date:
res[(date_from-start_date).days]['color'] = holiday.holiday_status_id.color_name
count+=1
date_from += timedelta(1)
count += abs(holiday.number_of_days)
self.sum = count
return res
File diff suppressed because it is too large Load Diff
@@ -55,7 +55,7 @@
<field name="parent_id" ref="hr_payroll.ALW"/>
</record>
<record id="hr_payroll_head_pp" model="hr.salary.rule.category">
<field name="name">Withholding Tax Grid 2017 (Precompte Professionnel Bareme 2017)</field>
<field name="name">Withholding Tax Grid 2018 (Precompte Professionnel Bareme 2018)</field>
<field name="code">PP</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
@@ -126,14 +126,14 @@ if contract.mobile:
<field name="code">EmpBonus.1</field>
<field name="sequence">42</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT &lt;= 2431.27</field>
<field name="condition_python">result = categories.BRUT &lt;= 2510.47</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
if salary &lt;= 1577.89:
result = 193.79
elif salary &lt;= 2461.27:
result = 193.79 - (0.2194 * (salary - 1577.89))
if salary &lt;= 1609.47:
result = 197.67
elif salary &lt;= 2510.47:
result = 197.67 - (0.2194 * (salary - 1609.47))
</field>
</record>
<record id="hr_salary_rule_employment_bonus_workers" model="hr.salary.rule">
@@ -142,14 +142,14 @@ elif salary &lt;= 2461.27:
<field name="code">EmpBonus.2</field>
<field name="sequence">42</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT + categories.ONSS &lt;= 2431.27</field>
<field name="condition_python">result = categories.BRUT + categories.ONSS &lt;= 2510.47</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
if salary &lt;= 1577.89:
result = 209.29
elif salary &lt;= 2461.27:
result = 209.29 - (0.2369 * (salary - 1577.89))
if salary &lt;= 1609.47:
result = 213.48
elif salary &lt;= 2510.47:
result = 213.48 - (0.2369 * (salary - 1609.47))
</field>
</record>
<record id="hr_payroll_rules_mis_ex_onss_1" model="hr.salary.rule">
@@ -160,7 +160,7 @@ elif salary &lt;= 2461.27:
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
<field name="condition_python">result = (((employee.marital=='single') or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
<field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
wage = categories.BASIC
@@ -208,7 +208,7 @@ else:
<field name="sequence">120</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
<field name="condition_python">result = (((employee.marital=='single') or (employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
<field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
</record>
<record id="hr_payroll_rules_baremeII" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
@@ -230,18 +230,6 @@ else:
<field name="condition_python">result = employee.resident_bool==True</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_salary_rule_pp_reduction_low_salary" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_pp_reductions"/>
<field name="name">Withholding Tax Reduction for Low Salaries</field>
<field name="code">PPRed.0</field>
<field name="sequence">131</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.GROSS &lt;= 2027.86 and categories.GROSS &gt; 565.02</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
result = 5.83
</field>
</record>
<record id="hr_salary_rule_pp_reduction_employees" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_pp_reductions"/>
<field name="name">Withholding Tax Reduction for Low Salaries (ONSS)</field>
@@ -318,7 +306,7 @@ elif salary &lt;= 2461.27:
<field name="code">Red.Iso</field>
<field name="sequence">144</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'single') and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="condition_python">result = (employee.marital in ['divorced', 'single', 'widower']) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(25.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
@@ -331,7 +319,7 @@ elif salary &lt;= 2461.27:
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'widower' or (employee.marital in ['divorced', 'single', 'widower'] and employee.dependent_children)) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
<field name="amount_python_compute">result = min(36.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_disabled" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
@@ -342,7 +330,7 @@ elif salary &lt;= 2461.27:
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.disabled) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
<field name="amount_python_compute">result = min(36.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_seniors" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
@@ -353,7 +341,7 @@ elif salary &lt;= 2461.27:
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.other_dependent_people and employee.dependent_seniors) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(71.0 * employee.dependent_seniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
<field name="amount_python_compute">result = min(73.0 * employee.dependent_seniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_juniors" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
@@ -364,7 +352,7 @@ elif salary &lt;= 2461.27:
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.other_dependent_people and employee.dependent_juniors) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.0 * employee.dependent_juniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
<field name="amount_python_compute">result = min(36.0 * employee.dependent_juniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_insufficient_net" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
@@ -373,7 +361,7 @@ elif salary &lt;= 2461.27:
<field name="code">Red.Spouse.Net</field>
<field name="sequence">148</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue &lt;= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue &lt;= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
@@ -384,7 +372,7 @@ elif salary &lt;= 2461.27:
<field name="code">Red.Spouse.Oth.Net</field>
<field name="sequence">149</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue &lt;= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue &lt;= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
@@ -397,7 +385,7 @@ elif salary &lt;= 2461.27:
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.disabled_spouse_bool==True) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">min(35.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
<field name="amount_python_compute">min(36.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_payroll_rules_parent_company_car" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_impos"/>
@@ -559,7 +547,6 @@ result = -contract.car_atn
ref('hr_payroll_rules_retained_net'),
ref('hr_salary_rule_commission_on_target'),
ref('hr_salary_rule_pp_reduction_employees'),
ref('hr_salary_rule_pp_reduction_low_salary'),
])]" name="rule_ids"/>
<field name="company_id" ref="base.main_company"/>
@@ -94,7 +94,7 @@ class HrContract(models.Model):
elif contract.transport_mode == 'public_transport':
contract.transport_employer_cost = contract.public_transport_reimbursed_amount
elif contract.transport_mode == 'others':
contract.transport_employer_cost == contract.others_reimbursed_amount
contract.transport_employer_cost = contract.others_reimbursed_amount
@api.depends('commission_on_target')
def _compute_warrants_cost(self):
@@ -176,6 +176,16 @@ class HrContract(models.Model):
contract.double_holidays = contract.wage * 0.92
contract.thirteen_month = contract.wage
@api.onchange('transport_mode')
def _onchange_transport_mode(self):
if self.transport_mode != 'company_car':
self.fuel_card = 0
self.company_car_total_depreciated_cost = 0
if self.transport_mode != 'others':
self.others_reimbursed_amount = 0
if self.transport_mode != 'public_transports':
self.public_transport_reimbursed_amount = 0
@api.onchange('mobile', 'mobile_plus')
def _onchange_mobile(self):
if self.mobile_plus and not self.mobile:
@@ -46,6 +46,13 @@ class HrContract(models.Model):
self.car_id = self.env['fleet.vehicle'].search([('driver_id', '=', self.employee_id.address_home_id.id)], limit=1)
return {'domain': {'car_id': self._get_available_cars_domain()}}
@api.onchange('transport_mode')
def _onchange_transport_mode(self):
super(HrContract, self)._onchange_transport_mode()
if self.transport_mode != 'company_car':
self.car_id = False
self.new_car_model_id = False
def _get_available_cars_domain(self):
return ['|', ('driver_id', '=', False), ('driver_id', '=', self.employee_id.address_home_id.id)]
@@ -165,6 +165,7 @@ class XmlDeclaration(models.TransientModel):
lines = self.env.cr.fetchall()
invoicelines_ids = [rec[0] for rec in lines]
invoicelines = self.env['account.invoice.line'].browse(invoicelines_ids)
for inv_line in invoicelines:
#Check type of transaction
@@ -190,6 +191,8 @@ class XmlDeclaration(models.TransientModel):
#comes from purchase
po_lines = self.env['purchase.order.line'].search([('invoice_lines', 'in', inv_line.id)], limit=1)
if po_lines:
if self._is_situation_triangular(company, po_line=po_lines):
continue
location = self.env['stock.location'].browse(po_lines.order_id._get_destination_location())
region_id = self.env['stock.warehouse'].get_regionid_from_locationid(location)
if region_id:
@@ -198,6 +201,8 @@ class XmlDeclaration(models.TransientModel):
#comes from sales
so_lines = self.env['sale.order.line'].search([('invoice_lines', 'in', inv_line.id)], limit=1)
if so_lines:
if self._is_situation_triangular(company, so_line=so_lines):
continue
saleorder = so_lines.order_id
if saleorder and saleorder.warehouse_id and saleorder.warehouse_id.region_id:
exreg = IntrastatRegion.browse(saleorder.warehouse_id.region_id.id).code
@@ -287,3 +292,24 @@ class XmlDeclaration(models.TransientModel):
dim = ET.SubElement(item, 'Dim')
dim.set('prop', prop)
dim.text = value
def _is_situation_triangular(self, company, po_line=False, so_line=False):
# Ignoring what is purchased and sold by us with a dropshipping route
# outside of our country, or completely within it
# https://www.nbb.be/doc/dq/f_pdf_ex/intra2017fr.pdf (§ 4.x)
dropship_pick_type = self.env.ref('stock_dropshipping.picking_type_dropship', raise_if_not_found=False)
if not dropship_pick_type:
return False
stock_move_domain = [('picking_type_id', '=', dropship_pick_type.id)]
if po_line:
stock_move_domain.append(('purchase_line_id', '=', po_line.id))
if so_line:
stock_move_domain.append(('procurement_id.sale_line_id', '=', so_line.id))
stock_move = self.env['stock.move'].search(stock_move_domain, limit=1)
return stock_move and (
(stock_move.partner_id.country_id.code != company.country_id.code and
stock_move.picking_partner_id.country_id.code != company.country_id.code) or
(stock_move.partner_id.country_id.code == company.country_id.code and
stock_move.picking_partner_id.country_id.code == company.country_id.code))
+9 -1
View File
@@ -381,6 +381,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.searchview = new SearchView(this, this.dataset, this.fields_view, options);
return this.searchview.appendTo($("<div>")).then(function () {
self.$searchview_buttons = self.searchview.$buttons.contents();
// manually call do_search to generate the initial domain and filter
// the messages in the default channel
self.searchview.do_search();
});
},
/**
@@ -908,7 +911,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
contexts: [session.user_context],
});
this.domain = result.domain;
this._fetchAndRenderThread();
if (this.channel) {
// initially (when _onSearch is called manually), there is no
// channel set yet, so don't try to fetch and render the thread as
// this will be done as soon as the default channel is set
this._fetchAndRenderThread();
}
},
/**
* @private
@@ -21,9 +21,9 @@ BasicModel.include({
* @param {Object} record - an element from the localData
* @param {string} fieldName
* @return {Deferred<Object>} the deferred is resolved with the
* invalidPartnerIds
* invalidPartnerIds and partnerNames
*/
_setInvalidMany2ManyTagsEmail: function (record, fieldName) {
_fetchSpecialMany2ManyTagsEmail: function (record, fieldName) {
var self = this;
var localID = (record._changes && fieldName in record._changes) ?
record._changes[fieldName] :
@@ -37,14 +37,26 @@ BasicModel.include({
}
});
var def;
if (invalidPartnerIds) {
if (invalidPartnerIds.length) {
// remove invalid partners
var changes = {operation: 'DELETE', ids: _.pluck(invalidPartnerIds, 'id')};
def = this._applyX2ManyChange(record, fieldName, changes);
}
return $.when(def).then(function () {
return $.when({
invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'),
list = self._applyX2ManyOperations(self.localData[localID]);
if (list.res_ids.length) {
def = self._rpc({
model: list.model,
method: 'name_get',
args: list.res_ids,
context: record.getContext({fieldName: fieldName}),
});
}
return $.when(def).then(function (names) {
return $.when({
invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'),
partnerNames: _.object(names),
});
});
});
},
@@ -54,7 +66,7 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({
fieldsToFetch: _.extend({}, M2MTags.prototype.fieldsToFetch, {
email: {type: 'char'},
}),
specialData: "_setInvalidMany2ManyTagsEmail",
specialData: "_fetchSpecialMany2ManyTagsEmail",
//--------------------------------------------------------------------------
// Private
@@ -132,14 +144,17 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({
* Override for res.partner
* name_get is dynamic (based on context) while display_name is static
* (stored)
*
* @override
* @private
*/
get_render_data: function(ids){
this.dataset.cancel_read();
return this.dataset.name_get(ids).then(function (names) {
return _.map(names, function(name) {
return {id: name[0], display_name: name[1]};
});
_getRenderTagsContext: function () {
var result = this._super.apply(this, arguments);
var partnerNames = this.record.specialData[this.name].partnerNames;
_.each(result.elements, function (partner) {
partner.display_name = partnerNames[partner.id];
});
return result;
},
});
+2 -2
View File
@@ -12,8 +12,8 @@
.o_chatter_topbar {
height: @odoo-statusbar-height;
position: relative;
margin: @grid-gutter-width*0.5 -@grid-gutter-width*0.5 0;
padding: 0 @grid-gutter-width*0.5;
margin: @grid-gutter-width*0.5 0 0;
padding: 0;
.o-flex-display();
> .btn {
+1 -1
View File
@@ -44,7 +44,7 @@
</dd>
<dt>Assigned to</dt>
<dd class="mb8">
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" height="18" width="18" class="img-circle mr4" t-att-title="activity.create_uid[1]"/>
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" height="18" width="18" class="img-circle mr4" t-att-title="activity.user_id[1]"/>
<b><t t-esc="activity.user_id[1]"/></b>
<em>, due on </em><span t-attf-class="o_activity_color_#{activity.state}"><t t-esc="activity.date_deadline.format(date_format)"/></span>
</dd>
+7 -2
View File
@@ -1250,8 +1250,9 @@ QUnit.module('FieldMany2ManyTagsEmail', {
});
QUnit.test('fieldmany2many tags email', function (assert) {
assert.expect(11);
assert.expect(13);
var done = assert.async();
var nameGottenIds = [[12], [12, 14]];
this.data.partner.records[0].timmy = [12, 14];
@@ -1271,7 +1272,11 @@ QUnit.test('fieldmany2many tags email', function (assert) {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method ==='read' && args.model === 'partner_type') {
if (route === "/web/dataset/call_kw/partner_type/name_get") {
assert.deepEqual(args.args, nameGottenIds.shift(),
"partner with email should be name_get'ed");
}
else if (args.method ==='read' && args.model === 'partner_type') {
assert.step(args.args[0]);
assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
}
+15 -1
View File
@@ -212,10 +212,24 @@ class StockMove(models.Model):
ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_reserved, 'qty_done': 0})
if float_compare(qty_to_add, 0, precision_rounding=self.product_uom.rounding) > 0:
# Search for a sub-location where the product is available. This might not be perfectly
# correct if the quantity available is spread in several sub-locations, but at least
# we should be closer to the reality. Anyway, no reservation is made, so it is still
# possible to change it afterwards.
quants = self.env['stock.quant']._gather(self.product_id, self.location_id, lot_id=lot, strict=False)
available_quantity = self.product_id.uom_id._compute_quantity(
self.env['stock.quant']._get_available_quantity(
self.product_id, self.location_id, lot_id=lot, strict=False
), self.product_uom
)
location_id = False
if float_compare(qty_to_add, available_quantity, precision_rounding=self.product_uom.rounding) < 0:
location_id = quants.filtered(lambda r: r.quantity > 0)[-1:].location_id
vals = {
'move_id': self.id,
'product_id': self.product_id.id,
'location_id': self.location_id.id,
'location_id': location_id.id if location_id else self.location_id.id,
'location_dest_id': self.location_dest_id.id,
'product_uom_qty': 0,
'product_uom_id': self.product_uom.id,
+42 -19
View File
@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.saas~18\n"
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-20 09:53+0000\n"
"PO-Revision-Date: 2017-09-20 09:53+0000\n"
"POT-Creation-Date: 2018-01-10 07:40+0000\n"
"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -40,64 +40,88 @@ msgstr ""
msgid "<i class=\"fa fa-trash\"/> Delete"
msgstr ""
#. module: payment
#: code:addons/payment/models/payment_acquirer.py:112
#, python-format
msgid "<i>Cancel,</i> Your payment has been cancelled."
msgstr ""
#. module: payment
#: code:addons/payment/models/payment_acquirer.py:108
#, python-format
msgid "<i>Done,</i> Your online payment has been successfully processed. Thank you for your order."
msgstr ""
#. module: payment
#: code:addons/payment/models/payment_acquirer.py:116
#, python-format
msgid "<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order."
msgstr ""
#. module: payment
#: code:addons/payment/models/payment_acquirer.py:104
#, python-format
msgid "<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet."
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:76
#: code:addons/payment/static/src/js/payment_form.js:219
#: code:addons/payment/static/src/js/payment_form.js:77
#: code:addons/payment/static/src/js/payment_form.js:229
#, python-format
msgid "<p>Please fill all the inputs required.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:180
#: code:addons/payment/static/src/js/payment_form.js:190
#, python-format
msgid "<p>Please select a payment method.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:274
#: code:addons/payment/static/src/js/payment_form.js:287
#, python-format
msgid "<p>Please select the option to add a new payment method.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:315
#: code:addons/payment/static/src/js/payment_form.js:328
#, python-format
msgid "<p>This card is currently linked to the following records:<p/>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:108
#: code:addons/payment/static/src/js/payment_form.js:120
#: code:addons/payment/static/src/js/payment_form.js:254
#: code:addons/payment/static/src/js/payment_form.js:267
#: code:addons/payment/static/src/js/payment_form.js:109
#: code:addons/payment/static/src/js/payment_form.js:121
#: code:addons/payment/static/src/js/payment_form.js:264
#: code:addons/payment/static/src/js/payment_form.js:277
#, python-format
msgid "<p>We are not able to add your payment method at the moment.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:298
#: code:addons/payment/static/src/js/payment_form.js:333
#: code:addons/payment/static/src/js/payment_form.js:311
#: code:addons/payment/static/src/js/payment_form.js:346
#, python-format
msgid "<p>We are not able to delete your payment method at the moment.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:155
#: code:addons/payment/static/src/js/payment_form.js:161
#: code:addons/payment/static/src/js/payment_form.js:162
#: code:addons/payment/static/src/js/payment_form.js:168
#, python-format
msgid "<p>We are not able to redirect you to the payment form.</p>"
msgstr ""
#. module: payment
#. openerp-web
#: code:addons/payment/static/src/js/payment_form.js:169
#: code:addons/payment/static/src/js/payment_form.js:179
#, python-format
msgid "<p>We're unable to process your payment.</p>"
msgstr ""
@@ -251,8 +275,7 @@ msgstr ""
#. module: payment
#: model:payment.acquirer,name:payment.payment_acquirer_authorize
#: selection:payment.acquirer,provider:0
msgid "Authorize.Net"
msgid "Authorize.net"
msgstr ""
#. module: payment
+10 -8
View File
@@ -102,19 +102,19 @@ class PaymentAcquirer(models.Model):
help='Message displayed after having done the payment process.')
pending_msg = fields.Html(
'Pending Message', translate=True,
default='<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.',
default=lambda s: _('<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.'),
help='Message displayed, if order is in pending state after having done the payment process.')
done_msg = fields.Html(
'Done Message', translate=True,
default='<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.',
default=lambda s: _('<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.'),
help='Message displayed, if order is done successfully after having done the payment process.')
cancel_msg = fields.Html(
'Cancel Message', translate=True,
default='<i>Cancel,</i> Your payment has been cancelled.',
default=lambda s: _('<i>Cancel,</i> Your payment has been cancelled.'),
help='Message displayed, if order is cancel during the payment process.')
error_msg = fields.Html(
'Error Message', translate=True,
default='<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.',
default=lambda s: _('<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.'),
help='Message displayed, if error is occur during the payment process.')
save_token = fields.Selection([
('none', 'Never'),
@@ -439,15 +439,17 @@ class PaymentIcon(models.Model):
@api.model
def create(self, vals):
if 'image' in vals:
vals['image_payment_form'] = image_resize_image(vals['image'], size=(45,30))
vals['image'] = image_resize_image(vals['image'], size=(64,64))
image = ustr(vals['image'] or '').encode('utf-8')
vals['image_payment_form'] = image_resize_image(image, size=(45,30))
vals['image'] = image_resize_image(image, size=(64,64))
return super(PaymentIcon, self).create(vals)
@api.multi
def write(self, vals):
if 'image' in vals:
vals['image_payment_form'] = image_resize_image(vals['image'], size=(45,30))
vals['image'] = image_resize_image(vals['image'], size=(64,64))
image = ustr(vals['image'] or '').encode('utf-8')
vals['image_payment_form'] = image_resize_image(image, size=(45,30))
vals['image'] = image_resize_image(image, size=(64,64))
return super(PaymentIcon, self).write(vals)
class PaymentTransaction(models.Model):
+1 -1
View File
@@ -78,7 +78,7 @@
</t>
</ul>
</div>
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="panel-footer {{'hidden' if (len(s2s_acquirers)&gt;1 and len(pms)==0 and s2s_acquirers[0]!=acq) else 'hidden' if len(pms)&gt;0 else ''}}">
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="panel-footer {{'hidden' if(acquirers_count &gt; 1 and pms_count==0 and s2s_acquirers[0]!=acq) else 'hidden' if pms_count &gt;0 else ''}}">
<div class="clearfix">
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
<t t-set="id" t-value="acq.id"/>
+1 -1
View File
@@ -416,7 +416,7 @@
<field name="arch" type="xml">
<form string="Payment Icon">
<sheet>
<field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image_medium"}'/>
<field name="image" widget="image" class="oe_avatar"/>
<div class="oe_title">
<h1><field name="name" placeholder="Name"/></h1>
</div>
+14 -9
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-13 14:36+0000\n"
"PO-Revision-Date: 2017-12-13 14:36+0000\n"
"POT-Creation-Date: 2018-01-08 10:30+0000\n"
"PO-Revision-Date: 2018-01-08 10:30+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -1193,6 +1193,11 @@ msgstr ""
msgid "Do you want to open cash registers?"
msgstr ""
#. module: point_of_sale
#: model:ir.ui.view,arch_db:point_of_sale.pos_config_view_form
msgid "Don't turn this option on if you take orders on smartphones or tablets. Such devices already benefit from a native keyboard."
msgstr ""
#. module: point_of_sale
#: model:ir.ui.view,arch_db:point_of_sale.view_pos_order_filter
msgid "Done"
@@ -1204,11 +1209,6 @@ msgid "Don’t turn this option on if you take orders on smartphones or tablets.
" Such devices already benefit from a native keyboard."
msgstr ""
#. module: point_of_sale
#: model:ir.ui.view,arch_db:point_of_sale.pos_config_view_form
msgid "Don’t turn this option on if you take orders on smartphones or tablets. Such devices already benefit from a native keyboard."
msgstr ""
#. module: point_of_sale
#. openerp-web
#: code:addons/point_of_sale/static/src/xml/pos.xml:1355
@@ -3713,18 +3713,23 @@ msgstr ""
#. module: point_of_sale
#. openerp-web
#: code:addons/point_of_sale/static/src/js/screens.js:2040
#: code:addons/point_of_sale/static/src/js/screens.js:2061
#, python-format
msgid "Unknown Error"
msgstr ""
#. module: point_of_sale
#. openerp-web
#: code:addons/point_of_sale/static/src/js/screens.js:1334
#: code:addons/point_of_sale/static/src/js/screens.js:1337
#, python-format
msgid "Unsupported File Format"
msgstr ""
#. module: point_of_sale
#: model:ir.ui.view,arch_db:point_of_sale.view_pos_config_kanban
msgid "Unused"
msgstr ""
#. module: point_of_sale
#: model:ir.model.fields,field_description:point_of_sale.field_pos_config_use_existing_lots
msgid "Use Existing Lots/Serial Numbers"
@@ -78,7 +78,8 @@
To Close
</div>
</t>
<div class="o_secondary mt16"><t t-esc="record.pos_session_username.value || 'Unused'"/></div>
<t t-set="unused_message">Unused</t>
<div class="o_secondary mt16"><t t-esc="record.pos_session_username.value || unused_message"/></div>
</div>
<div class="o_kanban_manage_button_section">
<a class="o_kanban_manage_toggle_button" href="#"><i class="fa fa-ellipsis-v" /></a>
+1 -1
View File
@@ -39,7 +39,7 @@
<li class="dropdown" t-ignore="true" t-if="not user_id._is_public()">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<b>
<span t-esc="(len(user_id.name)&gt;25) and (user_id.name[:23]+'...') if user_id.name else user_id.name"/>
<span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) &gt; 25 else user_id.name"/>
<span class="caret"></span>
</b>
</a>
+3 -3
View File
@@ -47,7 +47,7 @@
<field name="categ_id" string="Product Category"/>
</group>
<group name="group_standard_price">
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
<field name="company_id" groups="base.group_multi_company"
options="{'no_create': True}"/>
<field name="uom_id" groups="product.group_uom" options="{'no_create': True}"/>
@@ -214,8 +214,8 @@
<group>
<group name="pricing" string="Pricing">
<field name="product_variant_count" invisible="1"/>
<field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id'}" attrs="{'readonly': [('product_variant_count', '&gt;', 1)]}"/>
<field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('product_variant_count', '&gt;', 1)]}"/>
<field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
<field name="currency_id" invisible='1'/>
</group>
<group name="weight">
+7 -2
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-13 14:36+0000\n"
"PO-Revision-Date: 2017-12-13 14:36+0000\n"
"POT-Creation-Date: 2018-01-10 07:39+0000\n"
"PO-Revision-Date: 2018-01-10 07:39+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -548,6 +548,7 @@ msgid "Basic"
msgstr ""
#. module: project
#: code:addons/project/models/project.py:29
#: model:project.task,legend_blocked:project.project_task_data_0
#: model:project.task,legend_blocked:project.project_task_data_1
#: model:project.task,legend_blocked:project.project_task_data_11
@@ -564,6 +565,7 @@ msgstr ""
#: model:project.task.type,legend_blocked:project.project_stage_data_1
#: model:project.task.type,legend_blocked:project.project_stage_data_2
#: selection:report.project.task.user,state:0
#, python-format
msgid "Blocked"
msgstr ""
@@ -1114,6 +1116,7 @@ msgid "If the active field is set to False, it will allow you to hide the projec
msgstr ""
#. module: project
#: code:addons/project/models/project.py:35
#: model:project.task,legend_normal:project.project_task_data_0
#: model:project.task,legend_normal:project.project_task_data_1
#: model:project.task,legend_normal:project.project_task_data_11
@@ -1130,6 +1133,7 @@ msgstr ""
#: model:project.task.type,legend_normal:project.project_stage_data_1
#: model:project.task.type,legend_normal:project.project_stage_data_2
#: selection:report.project.task.user,state:0
#, python-format
msgid "In Progress"
msgstr ""
@@ -1650,6 +1654,7 @@ msgstr ""
#: model:ir.actions.act_window,name:project.rating_rating_action_view_project_rating
#: model:ir.ui.view,arch_db:project.view_task_form2
msgid "Rating"
#, python-format
msgstr ""
#. module: project
+3 -3
View File
@@ -27,13 +27,13 @@ class ProjectTaskType(models.Model):
string='Starred Explanation', translate=True,
help='Explanation text to help users using the star on tasks or issues in this stage.')
legend_blocked = fields.Char(
'Red Kanban Label', default='Blocked', translate=True, required=True,
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True,
help='Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage.')
legend_done = fields.Char(
'Green Kanban Label', default='Ready for Next Stage', translate=True, required=True,
'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, required=True,
help='Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage.')
legend_normal = fields.Char(
'Grey Kanban Label', default='In Progress', translate=True, required=True,
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True,
help='Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage.')
mail_template_id = fields.Many2one(
'mail.template',
@@ -59,7 +59,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml">
<field name="reference" position="after" >
<field name="purchase_id" attrs="{'invisible': [('state', '=', 'purchase')]}" class="oe_edit_only"
<field name="purchase_id" attrs="{'readonly': [('state','not in',['draft'])], 'invisible': [('state', '=', 'purchase')]}" class="oe_edit_only"
options="{'no_create': True}" context="{'show_total_amount': True}"/>
</field>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='company_id']" position="after">
+18
View File
@@ -826,6 +826,18 @@ class SaleOrderLine(models.Model):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
self.filtered(
lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0)._update_line_quantity(values)
# Prevent writing on a locked SO.
protected_fields = self._get_protected_fields()
if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields):
fields = self.env['ir.model.fields'].search([
('name', 'in', protected_fields), ('model', '=', self._name)
])
raise UserError(
_('It is forbidden to modify the following fields in a locked order:\n%s')
% '\n'.join(fields.mapped('field_description'))
)
result = super(SaleOrderLine, self).write(values)
return result
@@ -1112,6 +1124,12 @@ class SaleOrderLine(models.Model):
return product[field_name] * uom_factor * cur_factor, currency_id.id
def _get_protected_fields(self):
return [
'product_id', 'name', 'price_unit', 'product_uom', 'product_uom_qty',
'tax_id', 'analytic_tag_ids'
]
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
self.discount = 0.0
@@ -78,12 +78,14 @@ class SaleAdvancePaymentInv(models.TransientModel):
if self.amount <= 0.00:
raise UserError(_('The value of the down payment amount must be positive.'))
context = {'lang': order.partner_id.lang}
if self.advance_payment_method == 'percentage':
amount = order.amount_untaxed * self.amount / 100
name = _("Down payment of %s%%") % (self.amount,)
else:
amount = self.amount
name = _('Down Payment')
del context
taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
if order.fiscal_position_id and taxes:
tax_ids = order.fiscal_position_id.map_tax(taxes).ids
@@ -154,6 +156,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
tax_ids = order.fiscal_position_id.map_tax(taxes).ids
else:
tax_ids = taxes.ids
context = {'lang': order.partner_id.lang}
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
@@ -165,6 +168,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'tax_id': [(6, 0, tax_ids)],
'is_downpayment': True,
})
del context
self._create_invoice(order, so_line, amount)
if self._context.get('open_invoices', False):
return sale_orders.action_view_invoice()
+8 -1
View File
@@ -335,8 +335,15 @@ class TestSaleMrpFlow(common.TransactionCase):
product.invoice_policy = 'delivery'
# Remove the MTO route as purchase is not installed and since the procurement removal the exception is directly raised
product.write({'route_ids': [(6, 0, [self.warehouse.manufacture_pull_id.route_id.id])]})
partner = self.env.ref('base.res_partner_1')
# if `delivery` module is installed, a default property is set for the carrier to use
# However this will lead to an extra line on the SO (the delivery line), which will force
# the SO to have a different flow (and `invoice_state` value)
partner.property_delivery_carrier_id = False
f = Form(self.env['sale.order'])
f.partner_id = self.env.ref('base.res_partner_1')
f.partner_id = partner
with f.order_line.new() as line:
line.product_id = product
line.product_uom_qty = 5
+1
View File
@@ -13,6 +13,7 @@ class PaymentTransaction(models.Model):
# YTI FIXME: The auto_join seems useless
sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True)
so_state = fields.Selection('sale.order', string='Sale Order State', related='sale_order_id.state')
@api.model
def form_feedback(self, data, acquirer_name):
+2 -3
View File
@@ -176,14 +176,14 @@ class TestSaleStock(TestSale):
# Check invoice
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" after picking return' % self.so.invoice_status)
self.assertEqual(self.so.order_line[0].qty_delivered, 3.0, 'Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered)
self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, 'Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered)
# let's do an invoice with refunds
adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({
'advance_payment_method': 'all',
})
adv_wiz.with_context(open_invoices=True).create_invoices()
self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft')
self.assertEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, 'Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity)
self.assertAlmostEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, 'Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity)
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status)
def test_03_sale_stock_delivery_partial(self):
@@ -303,4 +303,3 @@ class TestSaleStock(TestSale):
self.assertEqual(len(so1.picking_ids), 2)
picking2 = so1.picking_ids.filtered(lambda p: p.state != 'cancel')
self.assertEqual(picking2.partner_id.id, partner2)
@@ -226,6 +226,7 @@ class SaleOrderLine(models.Model):
'project_id': project.id,
'sale_line_id': self.id,
'company_id': self.company_id.id,
'email_from': self.order_id.partner_id.email,
'user_id': False, # force non assigned task, as created as sudo()
}
@@ -37,6 +37,7 @@ class TestSaleService(CommonTest):
task = project.task_ids.filtered(lambda t: t.name == '%s:%s' % (sale_order.name, self.product_delivery_timesheet2.name))
self.assertTrue(task, 'Sale Service: task is not created')
self.assertEqual(task.partner_id, sale_order.partner_id, 'Sale Service: customer should be the same on task and on SO')
self.assertEqual(task.email_from, sale_order.partner_id.email, 'Sale Service: Task Email should be the same as the SO customer Email')
# register timesheet on task
self.env['account.analytic.line'].create({
'name': 'Test Line',
+16
View File
@@ -15,6 +15,22 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: stock
#: code:addons/stock/models/stock_move.py:230
#, python-format
msgid "\n"
"\n"
"%s --> Product UoM is %s (%s) - Move UoM is %s (%s)"
msgstr ""
#. module: stock
#: code:addons/stock/models/stock_move.py:231
#, python-format
msgid "\n"
"\n"
"Blocking: %s"
msgstr ""
#. module: stock
#: code:addons/stock/models/stock_move.py:308
#, python-format
@@ -72,6 +72,8 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def set_values(self):
super(ResConfigSettings, self).set_values()
if not self.user_has_groups('stock.group_stock_manager'):
return
self.env['ir.config_parameter'].sudo().set_param('stock.use_propagation_minimum_delta', self.use_propagation_minimum_delta)
""" If we are not in multiple locations, we can deactivate the internal
operation types of the warehouses, so they won't appear in the dashboard.
+4 -2
View File
@@ -316,10 +316,12 @@ class StockMove(models.Model):
@api.constrains('product_uom')
def _check_uom(self):
moves_error = self.filtered(lambda move: move.product_id.uom_id.category_id.id != move.product_uom.category_id.id)
moves_error = self.filtered(lambda move: move.product_id.uom_id.category_id != move.product_uom.category_id)
if moves_error:
user_warning = _('You try to move a product using a UoM that is not compatible with the UoM of the product moved. Please use an UoM in the same UoM category.')
user_warning += '\n\nBlocking: %s' % ' ,'.join(moves_error.mapped('name'))
for move in moves_error:
user_warning += _('\n\n%s --> Product UoM is %s (%s) - Move UoM is %s (%s)') % (move.product_id.display_name, move.product_id.uom_id.name, move.product_id.uom_id.category_id.name, move.product_uom.name, move.product_uom.category_id.name)
user_warning += _('\n\nBlocking: %s') % ' ,'.join(moves_error.mapped('name'))
raise UserError(user_warning)
@api.model_cr
+1 -1
View File
@@ -496,7 +496,7 @@ class StockMoveLine(models.Model):
candidate.product_qty - quantity,
precision_rounding=self.product_uom_id.rounding,
rounding_method='UP')
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, self.product_uom_id, rounding_method='HALF-UP')
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, candidate.product_uom_id, rounding_method='HALF-UP')
quantity -= quantity_split
move_to_recompute_state |= candidate.move_id
if quantity == 0.0:
+12 -5
View File
@@ -405,10 +405,12 @@ class Picking(models.Model):
"""
for picking in self:
packages = self.env['stock.quant.package']
for ml in picking.move_line_ids:
if ml.package_id.id == ml.result_package_id.id:
if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(ml.package_id):
packages |= ml.package_id
packages_to_check = picking.move_line_ids\
.filtered(lambda ml: ml.result_package_id and ml.package_id.id == ml.result_package_id.id)\
.mapped('package_id')
for package_to_check in packages_to_check:
if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(package_to_check):
packages |= package_to_check
picking.entire_package_ids = packages
picking.entire_package_detail_ids = packages
@@ -635,7 +637,12 @@ class Picking(models.Model):
self.write({'date_done': fields.Datetime.now()})
return True
do_transfer = action_done #TODO:replace later
# Backward compatibility
# Problem with fixed reference to a function:
# it doesn't allow for overriding action_done() through do_transfer
# get rid of me in master (and make me private ?)
def do_transfer(self):
return self.action_done()
def _check_move_lines_map_quant_package(self, package):
""" This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """
+3 -1
View File
@@ -455,6 +455,7 @@ class Warehouse(models.Model):
self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id),
self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)],
'company_id': warehouse.company_id.id,
}) for warehouse in self)
def _get_reception_delivery_route_values(self, route_type):
@@ -495,7 +496,8 @@ class Warehouse(models.Model):
'product_categ_selectable': True,
'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step',
'company_id': self.company_id.id,
'sequence': 20}
'sequence': 20,
}
# Pull / Push tools
# ------------------------------------------------------------
+41
View File
@@ -2690,6 +2690,47 @@ class StockMove(TransactionCase):
scrapped_move.quantity_done = 8
self.assertEqual(scrap.scrap_qty, 8, 'Scrap quantity is not updated.')
def test_scrap_5(self):
""" Scrap the product of a reserved move line where the product is reserved in another
unit of measure. Check that the move line is correctly updated after the scrap.
"""
# 4 units are available in stock
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 4)
# try to reserve a dozen
partner = self.env['res.partner'].create({'name': 'Kimberley'})
picking = self.env['stock.picking'].create({
'name': 'A single picking with one move to scrap',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'partner_id': partner.id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
})
move1 = self.env['stock.move'].create({
'name': 'A move to confirm and scrap its product',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_dozen.id,
'product_uom_qty': 1.0,
'picking_id': picking.id,
})
move1._action_confirm()
move1._action_assign()
self.assertEqual(move1.reserved_availability, 0.33)
# scrap a unit
scrap = self.env['stock.scrap'].create({
'product_id': self.product1.id,
'product_uom_id': self.product1.uom_id.id,
'scrap_qty': 1,
'picking_id': picking.id,
})
scrap.action_validate()
self.assertEqual(scrap.state, 'done')
self.assertEqual(move1.reserved_availability, 0.25)
def test_in_date_1(self):
""" Check that moving a tracked quant keeps the incoming date.
"""
+3 -1
View File
@@ -117,7 +117,9 @@ class ProductProduct(models.Model):
""" Changes the Standard Price of Product and creates an account move accordingly."""
AccountMove = self.env['account.move']
locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id)])
quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', 'in', self.ids)], ['location_id'], ['location_id'])
quant_loc_ids = [loc['location_id'][0] for loc in quant_locs]
locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id), ('id', 'in', quant_loc_ids)])
product_accounts = {product.id: product.product_tmpl_id.get_product_accounts() for product in self}
+1 -1
View File
@@ -41,7 +41,7 @@
<div class="o_row">
<field name="standard_price"
attrs="{'readonly':[('valuation','=','real_time')]}"
widget='monetary' options="{'currency_field': 'currency_id'}" />
widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" />
<button string="Update Cost" type="action"
name="%(action_view_change_standard_price)d"
attrs="{'invisible':['|', '|', ('valuation','!=', 'real_time'), ('cost_method', 'not in', ['average', 'standard']), ('product_variant_count', '>', 1)]}"
+78 -14
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-13 14:37+0000\n"
"PO-Revision-Date: 2017-12-13 14:37+0000\n"
"POT-Creation-Date: 2018-01-09 10:16+0000\n"
"PO-Revision-Date: 2018-01-09 10:16+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -520,6 +520,13 @@ msgstr ""
msgid "Bar Chart"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/fields/basic_fields.js:1311
#, python-format
msgid "Binary File"
msgstr ""
#. module: web
#: code:addons/web/controllers/main.py:1474
#, python-format
@@ -934,14 +941,21 @@ msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:505
#: code:addons/web/static/src/js/fields/basic_fields.js:1316
#, python-format
msgid "Download"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:550
#, python-format
msgid "Download xls"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/form/form_controller.js:161
#: code:addons/web/static/src/js/views/form/form_controller.js:162
#, python-format
msgid "Duplicate"
msgstr ""
@@ -966,6 +980,13 @@ msgstr ""
msgid "Edit Action"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:275
#, python-format
msgid "Edit Calendar view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/kanban/kanban_column.js:287
@@ -984,7 +1005,49 @@ msgstr ""
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:269
#, python-format
msgid "Edit SearchView"
msgid "Edit Form view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:273
#, python-format
msgid "Edit Gantt view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:272
#, python-format
msgid "Edit Graph view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:271
#, python-format
msgid "Edit Kanban view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:270
#, python-format
msgid "Edit List view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:274
#, python-format
msgid "Edit Pivot view"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:279
#, python-format
msgid "Edit Search view"
msgstr ""
#. module: web
@@ -994,6 +1057,13 @@ msgstr ""
msgid "Edit Stage"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:276
#, python-format
msgid "Edit view, type:"
msgstr ""
#. module: web
#: model:ir.ui.view,arch_db:web.login
msgid "Email"
@@ -2538,11 +2608,12 @@ msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/graph/graph_model.js:179
#: code:addons/web/static/src/js/views/graph/graph_model.js:178
#: code:addons/web/static/src/js/views/kanban/kanban_column.js:73
#: code:addons/web/static/src/js/views/kanban/kanban_column.js:76
#: code:addons/web/static/src/js/views/list/list_renderer.js:418
#: code:addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js:145
#: code:addons/web/static/src/js/views/list/list_renderer.js:421
#: code:addons/web/static/src/js/views/list/list_renderer.js:425
#: code:addons/web/static/src/js/views/pivot/pivot_model.js:689
#, python-format
msgid "Undefined"
@@ -2627,13 +2698,6 @@ msgstr ""
msgid "Validation Error"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:268
#, python-format
msgid "View"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:240
+17
View File
@@ -348,6 +348,23 @@ var utils = {
'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
].join(';');
},
/**
* Sort an array in place, keeping the initial order for identical values.
*
* @param {Array} array
* @param {function} iteratee
*/
stableSort: function (array, iteratee) {
var stable = array.slice();
return array.sort(function stableCompare (a, b) {
var order = iteratee(a, b);
if (order !== 0) {
return order;
} else {
return stable.indexOf(a) - stable.indexOf(b);
}
});
},
/**
* @param {any} array
* @param {any} elem1
@@ -582,6 +582,7 @@ var FieldMonetary = InputField.extend({
this.formatOptions.currency = this.currency;
this.formatOptions.digits = [16, 2];
this.formatOptions.field_digits = this.nodeOptions.field_digits;
},
//--------------------------------------------------------------------------
@@ -1311,6 +1312,25 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
},
});
var FieldBinaryDownloadLink = AbstractFieldBinary.extend({
template: 'FieldBinaryDownloadLink',
events: _.extend({}, AbstractFieldBinary.prototype.events, {
'click': function (ev) {
ev.stopPropagation();
},
}),
supportedFieldTypes: ['binary'],
init: function () {
this._super.apply(this, arguments);
this.filename = _t("Binary File");
if (this.nodeOptions.filename && this.recordData[this.nodeOptions.filename]) {
this.filename = this.recordData[this.nodeOptions.filename];
}
this.url = "data:application/octet-stream;base64," + this.value;
this.text = _t("Download");
},
});
var PriorityWidget = AbstractField.extend({
// the current implementation of this widget makes it
// only usable for fields of type selection
@@ -2541,6 +2561,7 @@ return {
TranslatableFieldMixin: TranslatableFieldMixin,
DebouncedField: DebouncedField,
FieldEmail: FieldEmail,
FieldBinaryDownloadLink: FieldBinaryDownloadLink,
FieldBinaryFile: FieldBinaryFile,
FieldBinaryImage: FieldBinaryImage,
FieldBoolean: FieldBoolean,
@@ -33,6 +33,7 @@ registry
.add('email', basic_fields.FieldEmail)
.add('phone', basic_fields.FieldPhone)
.add('url', basic_fields.UrlWidget)
.add('download_link', basic_fields.FieldBinaryDownloadLink)
.add('image', basic_fields.FieldBinaryImage)
.add('binary', basic_fields.FieldBinaryFile)
.add('monetary', basic_fields.FieldMonetary)
+20 -2
View File
@@ -27,6 +27,20 @@ var _t = core._t;
// Formatting
//------------------------------------------------------------------------------
/**
*
* @param {string} value base64 representation of the binary
* @param {Object} [field]
* a description of the field (note: this parameter is ignored)
* @param {Object} [options] additional options (note: this parameter is ignored)
*/
function formatBinary(value, field, options) {
if (!value) {
return '';
}
return utils.human_size(value.length / 1.37); // base64 estimated size: http://en.wikipedia.org/wiki/Base64#MIME
}
/**
* @todo Really? it returns a jQuery element... We should try to avoid this and
* let DOM utility functions handle this directly. And replace this with a
@@ -286,8 +300,12 @@ function formatMonetary(value, field, options) {
currency = session.get_currency(currency_id);
}
var digits = (currency && currency.digits) || options.digits;
if (options.field_digits === true) {
digits = field.digits || digits;
}
var formatted_value = formatFloat(value, field, {
digits: (currency && currency.digits) || options.digits,
digits: digits,
});
if (!currency || options.noSymbol) {
@@ -557,7 +575,7 @@ function parseMany2one(value) {
return {
format: {
binary: _.identity, // todo
binary: formatBinary,
boolean: formatBoolean,
char: formatChar,
date: formatDate,
@@ -1019,6 +1019,7 @@ var FieldX2Many = AbstractField.extend({
*/
_onEditLine: function (ev) {
ev.stopPropagation();
this.trigger_up('freeze_order', {id: this.value.id});
var editedRecord = this.value.data[ev.data.index];
this.renderer.setRowMode(editedRecord.id, 'edit')
.done(ev.data.onSuccess);
@@ -1105,6 +1106,7 @@ var FieldX2Many = AbstractField.extend({
*/
_onResequence: function (event) {
var self = this;
this.trigger_up('freeze_order', {id: this.value.id});
var rowIDs = event.data.rowIDs.slice();
var rowID = rowIDs.pop();
var defs = _.map(rowIDs, function (rowID, index) {
@@ -1124,6 +1126,11 @@ var FieldX2Many = AbstractField.extend({
operation: 'UPDATE',
id: rowID,
data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]),
}).always(function () {
self.trigger_up('toggle_column_order', {
id: self.value.id,
name: event.data.handleField,
});
});
});
},
@@ -1230,6 +1237,7 @@ var FieldOne2Many = FieldX2Many.extend({
}
} else if (!this.creatingRecord) {
this.creatingRecord = true;
this.trigger_up('freeze_order', {id: this.value.id});
this._setValue({
operation: 'CREATE',
position: this.editable,
@@ -365,12 +365,6 @@ return core.Class.extend({
child.attrs.modifiers.column_invisible || false;
}
});
// detect editables list has they behave differently with respect
// to the sorting (changes are not sorted directly)
if (mode === 'list' && view.arch.attrs.editable) {
attrs.keepChangesUnsorted = true;
}
}
}
if (attrs.Widget.prototype.fieldsToFetch) {
@@ -37,6 +37,7 @@ odoo.define('web.BasicModel', function (require) {
* offset: {integer},
* openGroupByDefault: {boolean},
* orderedBy: {Object[]},
* orderedResIDs: {integer[]},
* parentID: {string},
* rawContext: {Object},
* relationField: {string},
@@ -88,6 +89,7 @@ var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
var session = require('web.session');
var utils = require('web.utils');
var _t = core._t;
@@ -95,11 +97,13 @@ var x2ManyCommands = {
// (0, virtualID, {values})
CREATE: 0,
create: function (virtualID, values) {
delete values.id;
return [x2ManyCommands.CREATE, virtualID || false, values];
},
// (1, id, {values})
UPDATE: 1,
update: function (id, values) {
delete values.id;
return [x2ManyCommands.UPDATE, id, values];
},
// (2, id[, _])
@@ -410,29 +414,11 @@ var BasicModel = AbstractModel.extend({
return record;
}
// here, type === 'list'
// apply potential changes (only for x2many lists):
// for list having the flag _keepChangesUnsorted set to true (typically,
// editable lists), we sort before apply changes by default (to keep the
// changes unsorted, i.e. the edited line at the same place). However, if
// the user forces a sort, all changes done so far are sorted (this is the
// purpose of the _keepChangesUnsortedCount key), and changes done later
// on won't be sorted.
var count;
if (element._keepChangesUnsorted) {
// only apply a subset (by default 0) of changes before sorting
count = element._keepChangesUnsortedCount || 0;
element = this._applyX2ManyOperations(element, {to: count, position: 'bottom'});
} else {
// apply all changes before sorting
element = this._applyX2ManyOperations(element, {position: 'bottom'});
}
// apply potential changes (only for x2many lists)
element = this._applyX2ManyOperations(element);
this._sortList(element);
if (element._keepChangesUnsorted) {
// apply the remaining changes after the sort
element = this._applyX2ManyOperations(element, {from: count, position: 'bottom'});
}
if (element._changes) {
if (!element.orderedResIDs && element._changes) {
_.each(element._changes, function (change) {
if (change.operation === 'ADD' && change.isNew) {
element.data = _.without(element.data, change.id);
@@ -444,6 +430,7 @@ var BasicModel = AbstractModel.extend({
}
});
}
var list = {
aggregateValues: _.extend({}, element.aggregateValues),
context: _.extend({}, element.context),
@@ -921,6 +908,8 @@ var BasicModel = AbstractModel.extend({
// Erase changes as they have been applied
record._changes = {};
self.unfreezeOrder(record.id);
// Update the data directly or reload them
if (shouldReload) {
self._fetchRecord(record).then(function (record) {
@@ -952,6 +941,20 @@ var BasicModel = AbstractModel.extend({
record.fields = _.extend({}, record.fields, viewInfo.fields);
record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo);
},
/**
* For list resources, this freezes the current records order.
*
* @param {string} listID a valid element ID of type list
*/
freezeOrder: function (listID) {
var list = this.localData[listID];
if (list.type === 'record') {
return;
}
list = this._applyX2ManyOperations(list);
this._sortList(list);
this.localData[listID].orderedResIDs = list.res_ids;
},
/**
* Manually sets a resource as dirty. This is used to notify that a field
* has been modified, but with an invalid value. In that case, the value is
@@ -968,7 +971,7 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} list_id id for the list resource
* @param {string} fieldName valid field name
* @returns {this} so we can chain call this method
* @returns {Deferred}
*/
setSort: function (list_id, fieldName) {
var list = this.localData[list_id];
@@ -979,21 +982,26 @@ var BasicModel = AbstractModel.extend({
delete change.isNew;
});
}
// the user manually selected a sort order, so we sort the list with all
// the changes he made so far, even if it is editable, however, changes
// he'll made after won't be sorted if the list is editable
list._keepChangesUnsortedCount = list._changes ? list._changes.length : 0;
if (list.orderedBy.length === 0) {
list.orderedBy.push({name: fieldName, asc: true});
} else if (list.orderedBy[0].name === fieldName){
list.orderedBy[0].asc = !list.orderedBy[0].asc;
if (!list.orderedResIDs) {
list.orderedBy[0].asc = !list.orderedBy[0].asc;
}
} else {
var orderedBy = _.reject(list.orderedBy, function (o) {
return o.name === fieldName;
});
list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy);
}
return this;
list.orderedResIDs = null;
if (list.static) {
// sorting might require to fetch the field for records where the
// sort field is still unknown (i.e. on other pages for example)
return this._fetchUngroupedList(list);
}
return $.when();
},
/**
* Toggle the active value of given records (to archive/unarchive them)
@@ -1048,6 +1056,34 @@ var BasicModel = AbstractModel.extend({
});
}
},
/**
* For a list datapoint, unfreezes the current records order and sorts it.
* For a record datapoint, unfreezes the x2many list datapoints.
*
* @param {string} elementID a valid element ID
*/
unfreezeOrder: function (elementID) {
var list = this.localData[elementID];
if (list.type === 'record') {
var data = _.extend({}, list.data, list._changes);
for (var fieldName in data) {
var field = list.fields[fieldName];
if (!field || !data[fieldName]) {
continue;
}
if (field.type === 'one2many' || field.type === 'many2many') {
var recordlist = this.localData[data[fieldName]];
recordlist.orderedResIDs = null;
for (var index in recordlist.data) {
this.unfreezeOrder(recordlist.data[index]);
}
}
}
return;
}
list.orderedResIDs = null;
this._sortList(list);
},
//--------------------------------------------------------------------------
// Private
@@ -1060,7 +1096,7 @@ var BasicModel = AbstractModel.extend({
* not by list or kanban controllers.
*
* @private
* @param {string} listID a valid handle for a list object
* @param {Object} list a valid list object
* @param {Object} [options]
* @param {string} [options.position=top] if the new record should be added
* on top or on bottom of the list
@@ -1080,6 +1116,12 @@ var BasicModel = AbstractModel.extend({
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
var record = self.localData[id];
list._cache[record.res_id] = id;
if (list.orderedResIDs) {
var index = list.offset + (position !== 'top' ? list.limit : 0);
list.orderedResIDs.splice(index, 0, record.res_id);
// list could be a copy of the original one
self.localData[list.id].orderedResIDs = list.orderedResIDs;
}
return id;
});
},
@@ -1341,7 +1383,6 @@ var BasicModel = AbstractModel.extend({
// TODO: before registering the changes, verify that the x2many
// value has changed
record._changes[name] = list.id;
var shouldLoad = false;
list._changes = list._changes || [];
// save it in case of a [5] which will remove the _changes
@@ -1361,11 +1402,7 @@ var BasicModel = AbstractModel.extend({
}
if (command[0] === 1 && command[1]) {
// updating an existing record
recID = _.find(list.data, function (childID) {
var child = self.localData[childID];
return child.res_id === command[1];
});
rec = self.localData[recID];
rec = self.localData[list._cache[command[1]]];
}
if (!rec) {
var params = {
@@ -1391,18 +1428,25 @@ var BasicModel = AbstractModel.extend({
defs.push(self._applyOnChange(command[2], rec));
} else if (command[0] === 4) {
// LINK TO
// the dataPoint id will be set when the record will be fetched (for now,
// this dataPoint doesn't exist yet)
list._changes.push({operation: 'ADD', id: null, resID: command[1]});
shouldLoad = true;
rec = self.localData[list._cache[command[1]]];
if (rec) {
// modifications done on a record are discarded if
// the onchange uses a LINK TO
self.discardChanges(rec.id);
}
// the dataPoint id will be set when the record will be
// fetched (for now, this dataPoint may not exist yet)
list._changes.push({
operation: 'ADD',
id: rec ? rec.id : null,
resID: command[1],
});
} else if (command[0] === 5) {
// DELETE ALL
list._changes = [{operation: 'REMOVE_ALL'}];
}
});
if (shouldLoad) {
defs.push(self._readUngroupedList(list));
}
defs.push(self._readUngroupedList(list));
} else {
var newValue = self._parseServerValue(field, val);
if (newValue !== oldValue) {
@@ -1540,12 +1584,6 @@ var BasicModel = AbstractModel.extend({
if (idInCommands && change.operation === 'ADD') {
idsToRemove = _.without(idsToRemove, change.id);
}
// decrement _keepChangesUnsortedCount if we filter out an
// operation whose index is smaller than the count (as there
// will be one operation less to apply before sorting)
if (idInCommands && index < list._keepChangesUnsortedCount) {
list._keepChangesUnsortedCount--;
}
return idInCommands;
});
_.each(idsToRemove, function (id) {
@@ -1653,18 +1691,7 @@ var BasicModel = AbstractModel.extend({
break;
}
});
var idsInRange;
if (list.limit) {
idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
} else {
idsInRange = list.res_ids;
}
list.data = [];
_.each(idsInRange, function (id) {
if (list._cache[id]) {
list.data.push(list._cache[id]);
}
});
this._setDataInRange(list);
return list;
},
/**
@@ -2344,7 +2371,6 @@ var BasicModel = AbstractModel.extend({
rawContext: rawContext,
relationField: field.relation_field,
viewType: view ? view.type : fieldInfo.viewType,
_keepChangesUnsorted: fieldInfo.keepChangesUnsorted,
});
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
@@ -2624,6 +2650,7 @@ var BasicModel = AbstractModel.extend({
}
});
list = this._applyX2ManyOperations(list);
this._sortList(list);
if (type === 'many2many' || list._forceM2MLink) {
var relRecordCreated = _.filter(relRecordAdded, function (rec) {
return typeof rec.res_id === 'string';
@@ -2644,7 +2671,6 @@ var BasicModel = AbstractModel.extend({
_.each(relRecordUpdated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
if (!_.isEmpty(changes)) {
delete changes.id;
var command = x2ManyCommands.update(relRecord.res_id, changes);
commands[fieldName].push(command);
}
@@ -2664,7 +2690,6 @@ var BasicModel = AbstractModel.extend({
relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]});
changes = relRecord ? this._generateChanges(relRecord, options) : {};
if (!_.isEmpty(changes)) {
delete changes.id;
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
@@ -2675,7 +2700,7 @@ var BasicModel = AbstractModel.extend({
// this is a new id (maybe existing in DB, but new in JS)
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
if ('id' in changes) {
if (changes.id) {
// the subrecord already exists in db
delete changes.id;
if (this.isNew(record.id)) {
@@ -3065,7 +3090,7 @@ var BasicModel = AbstractModel.extend({
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
openGroupByDefault: params.openGroupByDefault,
orderedBy: params.orderedBy || [],
_keepChangesUnsorted: params._keepChangesUnsorted,
orderedResIDs: params.orderedResIDs,
parentID: params.parentID,
rawContext: params.rawContext,
ref: params.ref || res_id,
@@ -3444,6 +3469,81 @@ var BasicModel = AbstractModel.extend({
return record;
});
},
/**
* Reads data from server for all missing fields.
*
* @private
* @param {Object} list a valid resource object
* @param {interger[]} resIDs
* @param {string[]} fieldNames to check and read if missing
* @returns {Deferred<Object>}
*/
_readMissingFields: function (list, resIDs, fieldNames) {
var self = this;
var missingIDs = [];
for (var i = 0, len = resIDs.length; i < len; i++) {
var resId = resIDs[i];
var dataPointID = list._cache[resId];
if (!dataPointID) {
missingIDs.push(resId);
continue;
}
var record = self.localData[dataPointID];
var data = _.extend({}, record.data, record._changes);
if (_.difference(fieldNames, _.keys(data)).length) {
missingIDs.push(resId);
}
}
var def;
if (missingIDs.length && fieldNames.length) {
def = self._rpc({
model: list.model,
method: 'read',
args: [missingIDs, fieldNames],
context: list.getContext(),
});
} else {
def = $.when(_.map(missingIDs, function (id) {
return {id:id};
}));
}
return def.then(function (records) {
_.each(resIDs, function (id) {
var dataPoint;
var data = _.findWhere(records, {id: id});
if (id in list._cache) {
dataPoint = self.localData[list._cache[id]];
if (data) {
self._parseServerData(fieldNames, dataPoint, data);
_.extend(dataPoint.data, data);
}
} else {
dataPoint = self._makeDataPoint({
context: list.context,
data: data,
fieldsInfo: list.fieldsInfo,
fields: list.fields,
modelName: list.model,
parentID: list.id,
viewType: list.viewType,
});
self._parseServerData(fieldNames, dataPoint, dataPoint.data);
// add many2one records
list._cache[id] = dataPoint.id;
}
// set the dataPoint id in potential 'ADD' operation adding the current record
_.each(list._changes, function (change) {
if (change.operation === 'ADD' && !change.id && change.resID === id) {
change.id = dataPoint.id;
}
});
});
return list;
});
},
/**
* For a grouped list resource, this method fetches all group data by
* performing a /read_group. It also tries to read open subgroups if they
@@ -3503,6 +3603,7 @@ var BasicModel = AbstractModel.extend({
aggregateValues: aggregateValues,
groupedBy: list.groupedBy.slice(1),
orderedBy: list.orderedBy,
orderedResIDs: list.orderedResIDs,
limit: list.limit,
openGroupByDefault: list.openGroupByDefault,
parentID: list.id,
@@ -3555,70 +3656,44 @@ var BasicModel = AbstractModel.extend({
*/
_readUngroupedList: function (list) {
var self = this;
var def;
var ids = [];
var missingIds = [];
// generate the current count and res_ids list by applying the changes
var listWithChanges = this._applyX2ManyOperations(list);
var currentCount = listWithChanges.count;
var currentResIDs = listWithChanges.res_ids;
var upper_bound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upper_bound; i++) {
var id = currentResIDs[i];
ids.push(id);
if (!list._cache[id]) {
missingIds.push(id);
}
}
if (missingIds.length) {
if (fieldNames.length) {
def = this._rpc({
model: list.model,
method: 'read',
args: [missingIds, fieldNames],
context: list.getContext(),
});
} else {
def = $.when(_.map(missingIds, function (id) {
return {id:id};
}));
}
} else {
def = $.when();
}
return def.then(function (records) {
list.data = [];
_.each(ids, function (id) {
var dataPoint;
if (id in list._cache) {
dataPoint = self.localData[list._cache[id]];
} else {
dataPoint = self._makeDataPoint({
context: list.context,
data: _.findWhere(records, {id: id}),
fieldsInfo: list.fieldsInfo,
fields: list.fields,
modelName: list.model,
parentID: list.id,
viewType: list.viewType,
});
var def = $.when();
// add many2one records
self._parseServerData(fieldNames, dataPoint, dataPoint.data);
list._cache[id] = dataPoint.id;
}
// set the dataPoint id in potential 'ADD' operation adding the current record
_.each(list._changes, function (change) {
if (change.operation === 'ADD' && !change.id && change.resID === id) {
change.id = dataPoint.id;
}
// generate the current count and res_ids list by applying the changes
list = this._applyX2ManyOperations(list);
// for multi-pages list datapoints, we might need to read the
// order field first to apply the order on all pages
if (list.res_ids.length > list.limit) {
if (!list.orderedResIDs && list.orderedBy.length) {
var fieldNames = _.pluck(list.orderedBy, 'name');
def = this._readMissingFields(list, _.filter(list.res_ids, _.isNumber), fieldNames);
def.then(function () {
self._sortList(list);
});
if (_.contains(list.res_ids, id)) {
list.data.push(dataPoint.id);
}
}
return def.then(function () {
var resIDs = [];
// generate the current count and res_ids list by applying the changes
var currentCount = list.count;
var currentResIDs = list.res_ids;
var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upperBound; i++) {
var resId = currentResIDs[i];
if (_.isNumber(resId)) {
resIDs.push(resId);
}
}
return self._readMissingFields(list, resIDs, fieldNames).then(function () {
if (list.res_ids.length <= list.limit) {
self._sortList(list);
} else {
// sortList has already been applied after first the read
self._setDataInRange(list);
}
return list;
});
return list;
});
},
/**
@@ -3689,6 +3764,38 @@ var BasicModel = AbstractModel.extend({
return list;
});
},
/**
* Set data in range, i.e. according to the list offset and limit.
*
* @param {Object} list
*/
_setDataInRange: function (list) {
var idsInRange;
if (list.limit) {
idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
} else {
idsInRange = list.res_ids;
}
list.data = [];
_.each(idsInRange, function (id) {
if (list._cache[id]) {
list.data.push(list._cache[id]);
}
});
// display newly created record in addition to the displayed records
if (list.limit) {
for (var i = list.offset + list.limit; i < list.res_ids.length; i++) {
var id = list.res_ids[i];
var dataPointID = list._cache[id];
if (_.findWhere(list._changes, {isNew: true, id: dataPointID})) {
list.data.push(dataPointID);
} else {
break;
}
}
}
},
/**
* Change the offset of a record. Note that this does not reload the data.
* The offset is used to load a different record in a list of record (for
@@ -3720,14 +3827,28 @@ var BasicModel = AbstractModel.extend({
// only sort x2many lists
return;
}
var self = this;
if (list.orderedBy.length) {
var self = this;
if (list.orderedResIDs) {
var orderedResIDs = {};
for (var k = 0; k < list.orderedResIDs.length; k++) {
orderedResIDs[list.orderedResIDs[k]] = k;
}
utils.stableSort(list.res_ids, function compareResIdIndexes (resId1, resId2) {
if (!(resId1 in orderedResIDs) && !(resId2 in orderedResIDs)) {
return 0;
}
if (!(resId1 in orderedResIDs)) {
return Infinity;
}
if (!(resId2 in orderedResIDs)) {
return -Infinity;
}
return orderedResIDs[resId1] - orderedResIDs[resId2];
});
} else if (list.orderedBy.length) {
// sort records according to ordered_by[0]
var data = list.data;
var res_ids = list.res_ids;
var compareRecords = function (record1ID, record2ID, level) {
var compareRecords = function (resId1, resId2, level) {
if(!level) {
level = 0;
}
@@ -3735,6 +3856,17 @@ var BasicModel = AbstractModel.extend({
return 0;
}
var order = list.orderedBy[level];
var record1ID = list._cache[resId1];
var record2ID = list._cache[resId2];
if (!record1ID && !record2ID) {
return 0;
}
if (!record1ID) {
return Infinity;
}
if (!record2ID) {
return -Infinity;
}
var r1 = self.localData[record1ID];
var r2 = self.localData[record2ID];
var data1 = _.extend({}, r1.data, r1._changes);
@@ -3747,17 +3879,9 @@ var BasicModel = AbstractModel.extend({
}
return compareRecords(record1ID, record2ID, level + 1);
};
data.sort(compareRecords);
// sort res_ids accordingly (only the current range of ids, the one
// mapping the data, needs to be sorted)
var preRangeIDs = res_ids.slice(0, list.offset); // resIDs before the range
var postRangeIDs = res_ids.slice(list.offset + list.limit); // resIDs after the range
var rangeIDs = _.map(data, function (dataPointID) {
return self.localData[dataPointID].res_id;
});
list.res_ids = preRangeIDs.concat(rangeIDs).concat(postRangeIDs);
utils.stableSort(list.res_ids, compareRecords);
}
this._setDataInRange(list);
},
/**
* Updates the res_ids of the parent of a given element of type list.
@@ -53,7 +53,7 @@ var FieldManagerMixin = {
return self.model.save(dataPointID).then(function () {
return self._confirmSave(dataPointID);
});
} else {
} else if (options.notifyChange !== false) {
return self._confirmChange(dataPointID, result, event);
}
});
@@ -12,9 +12,10 @@ var qweb = core.qweb;
var FormController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
open_one2many_record: '_onOpenOne2ManyRecord',
bounce_edit: '_onBounceEdit',
button_clicked: '_onButtonClicked',
freeze_order: '_onFreezeOrder',
open_one2many_record: '_onOpenOne2ManyRecord',
open_record: '_onOpenRecord',
toggle_column_order: '_onToggleColumnOrder',
}),
@@ -298,6 +299,17 @@ var FormController = BasicController.extend({
state.id = env.currentId;
this._super(state);
},
/**
* Calls unfreezeOrder when changing the mode.
*
* @override
*/
_setMode: function (mode, recordID) {
if ((recordID || this.handle) === this.handle) {
this.model.unfreezeOrder(this.handle);
}
return this._super.apply(this, arguments);
},
/**
* Updates the controller's title according to the new state
*
@@ -444,6 +456,17 @@ var FormController = BasicController.extend({
_onEdit: function () {
this._setMode('edit');
},
/**
* This method is called when someone tries to freeze the order, most likely
* in a x2many list view
*
* @private
* @param {OdooEvent} event
*/
_onFreezeOrder: function (event) {
event.stopPropagation();
this.model.freezeOrder(event.data.id);
},
/**
* Opens a one2many record (potentially new) in a dialog. This handler is
* o2m specific as in this case, the changes done on the related record
@@ -518,10 +541,12 @@ var FormController = BasicController.extend({
*/
_onToggleColumnOrder: function (event) {
event.stopPropagation();
this.model.setSort(event.data.id, event.data.name);
var field = event.data.field;
var state = this.model.get(this.handle);
this.renderer.confirmChange(state, state.id, [field]);
var self = this;
this.model.setSort(event.data.id, event.data.name).then(function () {
var field = event.data.field;
var state = self.model.get(self.handle);
self.renderer.confirmChange(state, state.id, [field]);
});
},
});
@@ -485,8 +485,10 @@ var ListController = BasicController.extend({
if (!data.groupedBy) {
this.pager.updateState({current_min: 1});
}
this.model.setSort(data.id, event.data.name);
this.update({});
var self = this;
this.model.setSort(data.id, event.data.name).then(function () {
self.update({});
});
},
/**
* In a grouped list view, each group can be clicked on to open/close them.
@@ -276,6 +276,7 @@ var ListRenderer = BasicRenderer.extend({
}
if (node.attrs.widget || (options && options.renderWidgets)) {
var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
this._handleAttributes($el, node);
return $td.append($el);
}
var name = node.attrs.name;
@@ -286,6 +287,7 @@ var ListRenderer = BasicRenderer.extend({
escape: true,
isPassword: 'password' in node.attrs,
});
this._handleAttributes($td, node);
return $td.html(formattedValue);
},
/**
@@ -806,7 +806,7 @@ if (config.debug) {
// Instantiate the DebugManager and insert it into the DOM once dialog is opened
this.opened(function() {
self.debug_manager = new DebugManager(self);
var $header = self.$modal.find('.modal-header');
var $header = self.$modal.find('.modal-header:first');
return self.debug_manager.prependTo($header).then(function() {
self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
});
+17 -3
View File
@@ -265,8 +265,19 @@
</t>
<li><a href="#" data-action="fvg">Fields View Get</a></li>
<t t-if="can_edit">
<li><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.type)"/>View</a></li>
<li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit SearchView</a></li>
<li>
<a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">
<t t-if="view.type == 'form'">Edit Form view</t>
<t t-elif="view.type == 'list'">Edit List view</t>
<t t-elif="view.type == 'kanban'">Edit Kanban view</t>
<t t-elif="view.type == 'graph'">Edit Graph view</t>
<t t-elif="view.type == 'gantt'">Edit Gantt view</t>
<t t-elif="view.type == 'pivot'">Edit Pivot view</t>
<t t-elif="view.type == 'calendar'">Edit Calendar view</t>
<t t-else="1">Edit view, type: <t t-esc="_.str.capitalize(view.type)"/></t>
</a>
</li>
<li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit Search view</a></li>
</t>
</t>
</t>
@@ -786,7 +797,7 @@
<t t-foreach="elements" t-as="el">
<t t-set="color" t-value="el[colorField] || 0"/>
<span t-attf-class="badge dropdown o_tag_color_#{color}" t-att-data-color="color" t-att-data-index="el_index" t-att-data-id="el.id">
<span class="o_badge_text"><t t-raw="el.display_name"/></span>
<span class="o_badge_text"><t t-esc="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
</t>
@@ -895,6 +906,9 @@
</t>
</div>
</t>
<t t-name="FieldBinaryDownloadLink">
<a t-att-download="widget.filename" t-att-href="widget.url"><t t-esc="widget.text"/></a>
</t>
<t t-name="HiddenInputFile">
<div t-attf-class="o_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style">
<form class="o_form_binary_form" t-att-target="fileupload_id"
@@ -587,6 +587,52 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('float field with monetary widget and decimal precision', function (assert) {
assert.expect(5);
this.data.partner.records = [{
id: 1,
qux: -8.89859,
currency_id: 1,
}]
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<field name="qux" widget="monetary" options="{\'field_digits\': True}"/>' +
'<field name="currency_id" invisible="1"/>' +
'</sheet>' +
'</form>',
res_id: 1,
session: {
currencies: _.indexBy(this.data.currency.records, 'id'),
},
});
// Non-breaking space between the currency and the amount
assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0-8.9',
'The value should be displayed properly.');
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), '-8.9',
'The input should be rendered without the currency symbol.');
assert.strictEqual(form.$('input').parent().children().first().text(), '$',
'The input should be preceded by a span containing the currency symbol.');
form.$('input').val('109.2458938598598').trigger('input');
assert.strictEqual(form.$('input').val(), '109.2458938598598',
'The value should not be formated yet.');
form.$buttons.find('.o_form_button_save').click();
// Non-breaking space between the currency and the amount
assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0109.2',
'The new value should be rounded properly.');
form.destroy();
});
QUnit.module('FieldEmail');
QUnit.test('email field in form view', function (assert) {
@@ -1472,6 +1518,40 @@ QUnit.module('basic_fields', {
session.get_file = oldGetFile;
});
QUnit.test('download_link widget in list view', function (assert) {
assert.expect(5);
this.data.partner.fields.filename = { string: "Filename", type: "char" };
this.data.partner.records[0].filename = "document.txt";
var form = createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree string="Partners">' +
'<field name="document" widget="download_link" options="{\'filename\': \'filename\'}"/>' +
'<field name="filename"/>' +
'</tree>',
res_id: 1,
});
assert.ok(form.$('.o_data_cell').first().hasClass("o_download_link_cell"),
"this should be a download link cell");
assert.ok(form.$('.o_data_cell').first().find('a').eq(0),
"there should be a link in the cell");
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('download'),
"document.txt",
"it should download a file named 'document.txt'");
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('href').trim(),
"data:application/octet-stream;base64,coucou==",
"it should have the appropriate link of a binary");
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).text(),
"Download",
"the text of the link should be 'Download'");
form.destroy();
});
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
@@ -127,6 +127,15 @@ QUnit.test('format one2many', function(assert) {
assert.strictEqual(fieldUtils.format.one2many({data: [1, 2]}), '2 records');
});
QUnit.test('format binary', function (assert) {
assert.expect(1);
// base64 estimated size (bytes) = value.length / 1.37 (http://en.wikipedia.org/wiki/Base64#MIME)
// Here: 4 / 1.37 = 2.91970800 => 2.92 (rounded 2 decimals by utils.human_size)
assert.strictEqual(fieldUtils.format.binary('Cg=='), '2.92 Bytes');
});
QUnit.test('parse float', function(assert) {
assert.expect(10);
@@ -5,6 +5,7 @@ var AbstractField = require('web.AbstractField');
var BasicModel = require('web.BasicModel');
var concurrency = require('web.concurrency');
var FormView = require('web.FormView');
var ListRenderer = require('web.ListRenderer');
var ListView = require('web.ListView');
var relationalFields = require('web.relational_fields');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
@@ -2081,6 +2082,39 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('transferring class attributes in one2many sub fields', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="turtle_foo" class="hey"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('td.hey').length, 1,
'should have a td with the desired class');
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('td.hey').length, 1,
'should have a td with the desired class');
form.$('td.o_data_cell').click();
assert.strictEqual(form.$('input[name="turtle_foo"].hey').length, 1,
'should have an input with the desired class');
form.destroy();
});
QUnit.test('one2many with date and datetime', function (assert) {
assert.expect(2);
@@ -2238,7 +2272,15 @@ QUnit.module('relational_fields', {
});
QUnit.test('embedded one2many with handle widget', function (assert) {
assert.expect(7);
assert.expect(10);
var nbConfirmChange = 0;
testUtils.patch(ListRenderer, {
confirmChange: function () {
nbConfirmChange++;
return this._super.apply(this, arguments);
},
});
this.data.partner.records[0].turtles = [1, 2, 3];
@@ -2271,8 +2313,10 @@ QUnit.module('relational_fields', {
"should have the 3 rows in the correct order");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
"should still have the 3 rows in the correct order");
assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet");
// Drag and drop the second line in first position
testUtils.dragAndDrop(
@@ -2281,6 +2325,7 @@ QUnit.module('relational_fields', {
{position: 'top'}
);
assert.strictEqual(nbConfirmChange, 1, "should have confirmed changes only once");
assert.verifySteps(["0", "1"],
"sequences values should be incremental starting from the previous minimum one");
@@ -2288,9 +2333,20 @@ QUnit.module('relational_fields', {
"should have the 3 rows in the new order");
form.$buttons.find('.o_form_button_save').click();
assert.deepEqual(_.map(this.data.turtle.records, function(turtle) {
return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int');
}), [
{id: 1, turtle_foo: "yop", turtle_int: 1},
{id: 2, turtle_foo: "blip", turtle_int:0},
{id: 3, turtle_foo: "kawa", turtle_int:21}
], "should have save the changed sequence");
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
"should still have the 3 rows in the new order");
testUtils.unpatch(ListRenderer);
form.destroy();
});
@@ -2382,7 +2438,7 @@ QUnit.module('relational_fields', {
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1].turtles, [[1,1,{"turtle_int":2}],[4,2,false],[1,3,{"turtle_int":3}]],
assert.deepEqual(args.args[1].turtles, [[4,2,false],[1,1,{"turtle_int":2}],[1,3,{"turtle_int":3}]],
"should change all lines that have changed (the first one doesn't change because it has the same sequence)");
}
return this._super.apply(this, arguments);
@@ -2411,9 +2467,20 @@ QUnit.module('relational_fields', {
});
QUnit.test('onchange (with command 5) for embedded one2many with handle widget', function (assert) {
assert.expect(2);
assert.expect(3);
this.data.partner.records[0].turtles = [1, 2, 3];
var ids = [];
for (var i=10; i<50; i++) {
var id = 10 + i;
ids.push(id);
this.data.turtle.records.push({
id: id,
turtle_int: 0,
turtle_foo: "#" + id,
});
}
ids.push(1, 2, 3);
this.data.partner.records[0].turtles = ids;
this.data.partner.onchanges = {
turtles: function (obj) {
obj.turtles = [[5]].concat(obj.turtles);
@@ -2439,6 +2506,7 @@ QUnit.module('relational_fields', {
res_id: 1,
});
form.$('.o_pager_next').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
"should have the 3 rows in the correct order");
@@ -2453,7 +2521,10 @@ QUnit.module('relational_fields', {
{position: 'top'}
);
assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order");
form.$buttons.find('.o_form_button_save').click();
form.$('.o_pager_next').click();
assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip",
"should display to record in 'turtle_int' order");
@@ -2461,6 +2532,239 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('onchange with modifiers for embedded one2many on the second page', function (assert) {
assert.expect(7);
var data = this.data;
var ids = [];
for (var i=10; i<60; i++) {
var id = 10 + i;
ids.push(id);
data.turtle.records.push({
id: id,
turtle_int: 0,
turtle_foo: "#" + id,
});
}
ids.push(1, 2, 3);
data.partner.records[0].turtles = ids;
data.partner.onchanges = {
turtles: function (obj) {
// TODO: make this test more 'difficult'
// For now, the server only returns UPDATE commands (no LINK TO)
// even though it should do it (for performance reasons)
// var turtles = obj.turtles.splice(0, 20);
var turtles = [];
turtles.unshift([5]);
// create UPDATE commands for each records (this is the server
// usual answer for onchange)
for (var k in obj.turtles) {
var change = obj.turtles[k];
var record = _.findWhere(data.turtle.records, {id: change[1]});
if (change[0] === 1) {
_.extend(record, change[2]);
}
turtles.push([1, record.id, record]);
}
obj.turtles = turtles;
},
};
var form = createView({
View: FormView,
model: 'partner',
data: data,
arch:'<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="turtles">' +
'<tree editable="bottom" default_order="turtle_int" limit="10">' +
'<field name="turtle_int" widget="handle"/>' +
'<field name="turtle_foo"/>' +
'<field name="turtle_qux" attrs="{\'readonly\': [(\'turtle_foo\', \'=\', False)]}"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
"should display the records in order");
form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input');
// the domain fail if the widget does not use the allready loaded data.
form.$buttons.find('.o_form_button_cancel').click();
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "blurp#21#22#23#24#25#26#27#28#29",
"should display the records in order with the changes");
$('.modal .modal-footer button:first').click();
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
"should cancel changes and display the records in order");
form.$buttons.find('.o_form_button_edit').click();
// Drag and drop the third line in second position
testUtils.dragAndDrop(
form.$('.ui-sortable-handle').eq(2),
form.$('.o_field_one2many tbody tr').eq(1),
{position: 'top'}
);
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#30#31#32#33#34#35#36#37#38",
"should display the records in order after resequence (display record with turtle_int=0)");
// Drag and drop the third line in second position
testUtils.dragAndDrop(
form.$('.ui-sortable-handle').eq(2),
form.$('.o_field_one2many tbody tr').eq(1),
{position: 'top'}
);
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
"should display the records in order after resequence (display record with turtle_int=0)");
form.$buttons.find('.o_form_button_cancel').click();
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
"should display the records in order after resequence");
$('.modal .modal-footer button:first').click();
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
"should cancel changes and display the records in order");
form.destroy();
});
QUnit.test('onchange followed by edition on the second page', function (assert) {
assert.expect(20);
var ids = [];
for (var i=1; i<85; i++) {
var id = 10 + i;
ids.push(id);
this.data.turtle.records.push({
id: id,
turtle_int: id/3|0,
turtle_foo: "#" + i,
});
}
ids.splice(41, 0, 1, 2, 3);
this.data.partner.records[0].turtles = ids;
this.data.partner.onchanges = {
turtles: function (obj) {
obj.turtles = [[5]].concat(obj.turtles);
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="turtles">' +
'<tree editable="top" default_order="turtle_int">' +
'<field name="turtle_int" widget="handle"/>' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_pager_next').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input');
form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input');
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line");
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position");
assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line");
form.$('.o_data_row input:first').val('value 3').trigger('input');
assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position after onchange");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange");
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line");
form.destroy();
// bottom order
form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="turtles">' +
'<tree editable="bottom" default_order="turtle_int">' +
'<field name="turtle_int" widget="handle"/>' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_pager_next').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input');
form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input');
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line");
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position");
form.$('.o_data_row input:first').val('value 3').trigger('input');
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position");
form.destroy();
});
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
var done = assert.async();
assert.expect(20);
@@ -5137,6 +5441,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('id field in one2many in a new record', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="id" invisible="1"/>' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'create') {
var virtualID = args.args[0].turtles[0][1];
assert.deepEqual(args.args[0].turtles,
[[0, virtualID, {turtle_foo: "cat"}]],
'should send proper commands');
}
return this._super.apply(this, arguments);
},
});
form.$('td.o_field_x2many_list_row_add a').click();
form.$('td input[name="turtle_foo"]').val('cat').trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('sub form view with a required field', function (assert) {
assert.expect(2);
this.data.partner.fields.foo.required = true;
@@ -5629,9 +5965,9 @@ QUnit.module('relational_fields', {
// enable the many2many onchange and generate it
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
'should display the product xphone and xenomorphe');
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value');
// disable the many2many onchange
@@ -5661,19 +5997,19 @@ QUnit.module('relational_fields', {
// put list in readonly mode
form.$('input.o_field_integer[name="int_field"]').click();
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
'should display the product xphone and xenomorphe');
assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first recordsecond record",
assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "second recordfirst record",
'should display the tag in readonly (first record and second record)');
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value after save');
form.destroy();
@@ -6811,8 +7147,8 @@ QUnit.module('relational_fields', {
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
assert.strictEqual(args.args[1].turtles[42][0], 0);
assert.deepEqual(args.args[1].turtles[42][2], {turtle_foo: 'rainbow dash'});
assert.strictEqual(args.args[1].turtles[40][0], 0, 'should send a create command');
assert.deepEqual(args.args[1].turtles[40][2], {turtle_foo: 'rainbow dash'});
}
return this._super.apply(this, arguments);
},
@@ -6865,8 +7201,8 @@ QUnit.module('relational_fields', {
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
assert.strictEqual(args.args[1].turtles[0][0], 0);
assert.deepEqual(args.args[1].turtles[0][2], {turtle_foo: 'rainbow dash'});
assert.strictEqual(args.args[1].turtles[40][0], 0);
assert.deepEqual(args.args[1].turtles[40][2], {turtle_foo: 'rainbow dash'});
}
return this._super.apply(this, arguments);
},
@@ -6882,7 +7218,8 @@ QUnit.module('relational_fields', {
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('tr.o_data_row').length, 3,
"should have 3 data rows on the current page");
"should have 3 data rows on the current page (2 records and the created line)");
assert.ok(form.$('tr.o_data_row').first().hasClass('o_selected_row'),
"first row should be selected");
+20 -11
View File
@@ -989,7 +989,7 @@ QUnit.module('Views', {
});
QUnit.test('use default_order on editable tree: sort on demand', function (assert) {
assert.expect(8);
assert.expect(11);
this.data.foo.records[0].o2m = [1, 3];
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
@@ -1029,12 +1029,21 @@ QUnit.module('Views', {
"Value 2 should be third (shouldn't be sorted)");
form.$('.o_form_sheet_bg').click(); // validate the row before sorting
$o2m.find('.o_column_sortable').click();
assert.ok(form.$('tbody tr:first td:contains(Value 3)').length,
"Value 3 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
$o2m.find('.o_column_sortable').click(); // resort list after edition
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 1',
"Value 1 should be first");
assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
"Value 2 should be second (should be sorted after saving)");
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 1)').length,
assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 3',
"Value 3 should be third");
$o2m.find('.o_column_sortable').click();
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 3',
"Value 3 should be first");
assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
"Value 2 should be second (should be sorted after saving)");
assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 1',
"Value 1 should be third");
form.destroy();
@@ -1051,7 +1060,7 @@ QUnit.module('Views', {
ids.push(id);
this.data.bar.records.push({
id: id,
name: "Value " + id,
name: "Value " + (id < 10 ? '0' : '') + id,
});
}
this.data.foo.records[0].o2m = ids;
@@ -1074,15 +1083,15 @@ QUnit.module('Views', {
// Change page
form.$('.o_pager_next').click();
assert.ok(form.$('tbody tr:first td:contains(Value 44)').length,
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 44',
"record 44 should be first");
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 48)').length,
assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 48',
"record 48 should be last");
form.$('.o_column_sortable').click();
assert.ok(form.$('tbody tr:first td:contains(Value 48)').length,
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 08',
"record 48 should be first");
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 44)').length,
assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 04',
"record 44 should be first");
form.destroy();
@@ -814,7 +814,9 @@ registry.background = SnippetOption.extend({
var $image = $('<img/>', {class: 'hidden', src: value}).appendTo(this.$target);
var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
_editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
_editor.opened(function () {
_editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
});
_editor.on('save', this, function () {
this._setCustomBackground($image.attr('src'));
+1
View File
@@ -11,3 +11,4 @@ from . import res_company
from . import res_partner
from . import res_config_settings
from . import ir_model_fields
from . import ir_model
+15
View File
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import api, models
# This is a nasty hack, targeted for V11 only
class IrModel(models.Model):
_inherit = 'ir.model'
@api.multi
def unlink(self):
self.env.cr.execute(
"DELETE FROM ir_model_fields WHERE name='website_id'")
return super(IrModel, self).unlink()
@@ -13,6 +13,7 @@ class ResConfigSettings(models.TransientModel):
def _default_website(self):
return self.env['website'].search([], limit=1)
# FIXME: Set website_id to ondelete='cascade' in master
website_id = fields.Many2one('website', string="website", default=_default_website, required=True)
website_name = fields.Char('Website Name', related='website_id.name')
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang')
@@ -15,6 +15,10 @@
<field name="groups_id" eval="[(4, ref('group_website_designer'))]"/>
</record>
<record id="base.group_system" model="res.groups">
<field name="implied_ids" eval="[(4, ref('website.group_website_designer'))]"/>
</record>
<record id="website_designer_edit_qweb" model="ir.rule">
<field name="name">website_designer: Manage Website and qWeb view</field>
<field name="model_id" ref="base.model_ir_ui_view"/>
@@ -434,7 +434,6 @@ body .modal {
}
#theme_customize_modal {
overflow: visible;
z-index: 1020;
display: block;
.modal-dialog {
.o-position-absolute(@odoo-navbar-height + 10, 10px);
+4 -2
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-24 09:00+0000\n"
"PO-Revision-Date: 2017-10-24 09:00+0000\n"
"POT-Creation-Date: 2018-01-10 07:40+0000\n"
"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -2401,7 +2401,9 @@ msgid "This community is for professional and enthusiast users,\n"
msgstr ""
#. module: website_forum
#: code:addons/website_forum/models/forum.py:51
#: model:forum.forum,description:website_forum.forum_help
#, python-format
msgid "This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together."
msgstr ""
+14 -7
View File
@@ -48,9 +48,9 @@ class Forum(models.Model):
description = fields.Text(
'Description',
translate=True,
default='This community is for professionals and enthusiasts of our products and services. '
'Share and discuss the best content and new marketing ideas, '
'build your professional profile and become a better marketer together.')
default=lambda s: _('This community is for professionals and enthusiasts of our products and services. '
'Share and discuss the best content and new marketing ideas, '
'build your professional profile and become a better marketer together.'))
welcome_message = fields.Html(
'Welcome Message',
default = """<section class="bg-info" style="height: 168px;"><div class="container">
@@ -437,7 +437,7 @@ class Post(models.Model):
elif post.parent_id and not post.can_answer:
raise KarmaError('Not enough karma to answer to a question')
if not post.parent_id and not post.can_post:
post.state = 'pending'
post.sudo().state = 'pending'
# add karma for posting new questions
if not post.parent_id and post.state == 'active':
@@ -466,11 +466,18 @@ class Post(models.Model):
@api.multi
def write(self, vals):
trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually
if 'content' in vals:
vals['content'] = self._update_content(vals['content'], self.forum_id.id)
if 'state' in vals:
if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self):
raise KarmaError('Not enough karma to close or reopen a post.')
if vals['state'] in ['active', 'close']:
if any(not post.can_close for post in self):
raise KarmaError('Not enough karma to close or reopen a post.')
trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id']
elif vals['state'] == 'flagged':
if any(not post.can_flag for post in self):
raise KarmaError('Not enough karma to flag a post.')
trusted_keys += ['state', 'flag_user_id']
if 'active' in vals:
if any(not post.can_unlink for post in self):
raise KarmaError('Not enough karma to delete or reactivate a post')
@@ -487,7 +494,7 @@ class Post(models.Model):
tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
raise KarmaError(_('Not enough karma to retag.'))
if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id', 'tag_ids'] for key in vals) and any(not post.can_edit for post in self):
if any(key not in trusted_keys for key in vals) and any(not post.can_edit for post in self):
raise KarmaError('Not enough karma to edit a post.')
res = super(Post, self).write(vals)
+3 -3
View File
@@ -5,9 +5,9 @@
<div>
<div t-if="'karma' in fields" class='css_editable_mode_hidden'>
<div t-if="options.get('UserBio')">
<span t-field="object.company_id.name" class="o_forum_tooltip_line"/><br/>
<a t-att-href="object.company_id.website" t-if="object.company_id.website">
<span t-field="object.company_id.website" class="o_forum_tooltip_line"/>
<span t-field="object.company_name" class="o_forum_tooltip_line"/><br/>
<a t-att-href="object.website" t-if="object.website">
<span t-field="object.website" class="o_forum_tooltip_line"/>
</a>
</div>
<b><t t-esc="object.karma"/></b>
+2 -1
View File
@@ -259,7 +259,8 @@ class SaleOrderOption(models.Model):
order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
if order_line:
order_line[0].product_uom_qty += 1
order_line = order_line[0]
order_line.product_uom_qty += 1
else:
vals = {
'price_unit': self.price_unit,
@@ -1227,6 +1227,7 @@ msgstr ""
#. module: website_sale
#: model:ir.ui.view,arch_db:website_sale.payment
#: model:ir.ui.view,arch_db:website_sale.payment
msgid "Pay Now <span class=\"fa fa-long-arrow-right\"/>"
msgstr ""
+1 -1
View File
@@ -1317,7 +1317,7 @@
<h3 class="mb24">Pay with </h3>
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="submit_txt" t-value="'Pay Now'"/>
<t t-set="submit_txt">Pay Now</t>
<t t-set="icon_class" t-value="'fa-lock'"/>
<t t-set="pms" t-value="tokens"/>
<t t-set="form_action" t-value="'/shop/payment/token'"/>
@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.saas~18\n"
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-02 11:27+0000\n"
"PO-Revision-Date: 2017-10-02 11:27+0000\n"
"POT-Creation-Date: 2018-01-10 07:40+0000\n"
"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -195,6 +195,12 @@ msgstr ""
msgid "<i class=\"fa fa-thumbs-up\"/> Likes"
msgstr ""
#. module: website_slides
#: code:addons/website_slides/models/slides.py:105
#, python-format
msgid "<p>This channel is private and its content is restricted to some users.</p>"
msgstr ""
#. module: website_slides
#: model:ir.ui.view,arch_db:website_slides.slide_detail_view
msgid "<span class=\"badge\" id=\"facebook-badge\">0</span>\n"
+2 -1
View File
@@ -102,7 +102,8 @@ class Channel(models.Model):
string='Channel Groups', help="Groups allowed to see presentations in this channel")
access_error_msg = fields.Html(
'Error Message', help="Message to display when not accessible due to access rights",
default="<p>This channel is private and its content is restricted to some users.</p>", translate=html_translate, sanitize_attributes=False)
default=lambda s: _("<p>This channel is private and its content is restricted to some users.</p>"),
translate=html_translate, sanitize_attributes=False)
upload_group_ids = fields.Many2many(
'res.groups', 'rel_upload_groups', 'channel_id', 'group_id',
string='Upload Groups', help="Groups allowed to upload presentations in this channel. If void, every user can upload.")
+2
View File
@@ -860,6 +860,8 @@ class IrModelFields(models.Model):
attrs['column1'] = field_data['column1'] or col1
attrs['column2'] = field_data['column2'] or col2
attrs['domain'] = safe_eval(field_data['domain'] or '[]')
elif field_data['ttype'] == 'monetary' and not self.pool.loaded:
return
# add compute function if given
if field_data['compute']:
attrs['compute'] = make_compute(field_data['compute'], field_data['depends'])
+1 -1
View File
@@ -258,7 +258,7 @@ class TestTranslation(TransactionCase):
category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'})
category_no = category.with_context({})
self.assertEqual(category_no.name, 'Customers', "Duplication erased original untranslated value")
self.assertEqual(category_no.name, 'Clients (copie)', "Duplication should set untranslated value")
category_fr = category.with_context({'lang': 'fr_FR'})
self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value")
+3 -1
View File
@@ -3821,7 +3821,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
del vals['module'] # duplicated vals is not linked to any module
vals['res_id'] = target_id
if vals['lang'] == old.env.lang and field.translate is True:
vals['source'] = old_wo_lang[name]
# force a source if the new_val was not changed by copy override
if new_val == old[name]:
vals['source'] = old_wo_lang[name]
# the value should be the new value (given by copy())
vals['value'] = new_val
Translation.create(vals)