[MERGE] forward port branch saas-11.1 up to f6ef0c7760
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 <= 2431.27</field>
|
||||
<field name="condition_python">result = categories.BRUT <= 2510.47</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
salary = categories.BRUT
|
||||
if salary <= 1577.89:
|
||||
result = 193.79
|
||||
elif salary <= 2461.27:
|
||||
result = 193.79 - (0.2194 * (salary - 1577.89))
|
||||
if salary <= 1609.47:
|
||||
result = 197.67
|
||||
elif salary <= 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 <= 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 <= 2431.27</field>
|
||||
<field name="condition_python">result = categories.BRUT + categories.ONSS <= 2510.47</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
salary = categories.BRUT
|
||||
if salary <= 1577.89:
|
||||
result = 209.29
|
||||
elif salary <= 2461.27:
|
||||
result = 209.29 - (0.2369 * (salary - 1577.89))
|
||||
if salary <= 1609.47:
|
||||
result = 213.48
|
||||
elif salary <= 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 <= 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 <= 2027.86 and categories.GROSS > 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 <= 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 < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital in ['divorced', 'single', 'widower']) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 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 <= 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 < 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 <= 2461.27:
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (employee.disabled) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 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 <= 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 < 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 <= 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 < 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 <= 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 <= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue <= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 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 <= 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 <= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 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 <= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 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 <= 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 < 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))
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)>1 and len(pms)==0 and s2s_acquirers[0]!=acq) else 'hidden' if len(pms)>0 else ''}}">
|
||||
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="panel-footer {{'hidden' if(acquirers_count > 1 and pms_count==0 and s2s_acquirers[0]!=acq) else 'hidden' if pms_count >0 else ''}}">
|
||||
<div class="clearfix">
|
||||
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
|
||||
<t t-set="id" t-value="acq.id"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)>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) > 25 else user_id.name"/>
|
||||
<span class="caret"></span>
|
||||
</b>
|
||||
</a>
|
||||
|
||||
@@ -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', '>', 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', '>', 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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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. """
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user