[MERGE] forward port branch 11.0 up to 2835d29979
This commit is contained in:
@@ -412,7 +412,7 @@ class AccountJournal(models.Model):
|
||||
belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",)
|
||||
|
||||
# Bank journals fields
|
||||
bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]")
|
||||
bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False)
|
||||
bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined')
|
||||
bank_acc_number = fields.Char(related='bank_account_id.acc_number')
|
||||
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id')
|
||||
@@ -522,11 +522,16 @@ class AccountJournal(models.Model):
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
for journal in self:
|
||||
company = journal.company_id
|
||||
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
|
||||
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
|
||||
raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
|
||||
if self.bank_account_id:
|
||||
self.bank_account_id.company_id = vals['company_id']
|
||||
company = self.env['res.company'].browse(vals['company_id'])
|
||||
if self.bank_account_id.company_id and self.bank_account_id.company_id != company:
|
||||
self.bank_account_id.write({
|
||||
'company_id': company.id,
|
||||
'partner_id': company.partner_id.id,
|
||||
})
|
||||
if ('code' in vals and journal.code != vals['code']):
|
||||
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
|
||||
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
|
||||
@@ -542,8 +547,14 @@ class AccountJournal(models.Model):
|
||||
self.default_credit_account_id.currency_id = vals['currency_id']
|
||||
if self.bank_account_id:
|
||||
self.bank_account_id.currency_id = vals['currency_id']
|
||||
if 'bank_account_id' in vals and not vals.get('bank_account_id'):
|
||||
raise UserError(_('You cannot remove the bank account from the journal once set.'))
|
||||
if 'bank_account_id' in vals:
|
||||
if not vals.get('bank_account_id'):
|
||||
raise UserError(_('You cannot remove the bank account from the journal once set.'))
|
||||
else:
|
||||
bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id'])
|
||||
if bank_account.partner_id != company.partner_id:
|
||||
raise UserError(_("The partners of the journal's company and the related bank account mismatch."))
|
||||
|
||||
result = super(AccountJournal, self).write(vals)
|
||||
|
||||
# Create the bank_account_id if necessary
|
||||
|
||||
@@ -224,7 +224,7 @@ class AccountInvoice(models.Model):
|
||||
@api.depends('move_id.line_ids.amount_residual')
|
||||
def _compute_payments(self):
|
||||
payment_lines = set()
|
||||
for line in self.move_id.line_ids:
|
||||
for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id):
|
||||
payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id'))
|
||||
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
|
||||
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
|
||||
@@ -267,7 +267,6 @@ class AccountInvoice(models.Model):
|
||||
], string='Status', index=True, readonly=True, default='draft',
|
||||
track_visibility='onchange', copy=False,
|
||||
help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
|
||||
" * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n"
|
||||
" * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n"
|
||||
" * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
|
||||
" * The 'Cancelled' status is used when user cancel invoice.")
|
||||
|
||||
@@ -472,7 +472,7 @@ class AccountMoveLine(models.Model):
|
||||
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict')
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account')
|
||||
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
|
||||
company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True)
|
||||
company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True)
|
||||
counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.")
|
||||
|
||||
# TODO: put the invoice link and partner_id on the account_move
|
||||
@@ -525,7 +525,7 @@ class AccountMoveLine(models.Model):
|
||||
raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field."))
|
||||
|
||||
@api.multi
|
||||
@api.constrains('amount_currency')
|
||||
@api.constrains('amount_currency', 'debit', 'credit')
|
||||
def _check_currency_amount(self):
|
||||
for line in self:
|
||||
if line.amount_currency:
|
||||
@@ -936,7 +936,9 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
def _get_pair_to_reconcile(self):
|
||||
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
|
||||
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
|
||||
company_currency_id = self[0].account_id.company_id.currency_id
|
||||
account_curreny_id = self[0].account_id.currency_id
|
||||
field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
|
||||
#reconciliation on bank accounts are special cases as we don't want to set them as reconcilable
|
||||
#but we still want to reconcile entries that are reversed together in order to clear those lines
|
||||
#in the bank reconciliation report.
|
||||
@@ -974,8 +976,9 @@ class AccountMoveLine(models.Model):
|
||||
#there is no more pair to reconcile so return what move_line are left
|
||||
if not sm_credit_move or not sm_debit_move:
|
||||
return self
|
||||
|
||||
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
|
||||
company_currency_id = self[0].account_id.company_id.currency_id
|
||||
account_curreny_id = self[0].account_id.currency_id
|
||||
field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
|
||||
if not sm_debit_move.debit and not sm_debit_move.credit:
|
||||
#both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry
|
||||
field = 'amount_residual_currency'
|
||||
|
||||
@@ -738,6 +738,9 @@ class WizardMultiChartsAccounts(models.TransientModel):
|
||||
res.setdefault('domain', {})
|
||||
res['domain']['sale_tax_id'] = repr(sale_tax_domain)
|
||||
res['domain']['purchase_tax_id'] = repr(purchase_tax_domain)
|
||||
else:
|
||||
self.sale_tax_id = False
|
||||
self.purchase_tax_id = False
|
||||
if self.chart_template_id.transfer_account_id:
|
||||
self.transfer_account_id = self.chart_template_id.transfer_account_id.id
|
||||
if self.chart_template_id.code_digits:
|
||||
|
||||
@@ -36,12 +36,12 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_account_budget = fields.Boolean(string='Budget Management')
|
||||
module_account_payment = fields.Boolean(string='Online Payment')
|
||||
module_account_reports = fields.Boolean("Dynamic Reports")
|
||||
module_account_reports_followup = fields.Boolean("Enable payment followup management")
|
||||
module_account_reports_followup = fields.Boolean("Follow-up Levels")
|
||||
module_account_check_printing = fields.Boolean("Allow check printing and deposits")
|
||||
module_account_batch_deposit = fields.Boolean(string='Use batch deposit',
|
||||
help='This allows you to group received checks before you deposit them to the bank.\n'
|
||||
'-This installs the module account_batch_deposit.')
|
||||
module_account_sepa = fields.Boolean(string='Use SEPA payments')
|
||||
module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)')
|
||||
module_account_sepa_direct_debit = fields.Boolean(string='Use SEPA Direct Debit')
|
||||
module_account_plaid = fields.Boolean(string="Plaid Connector")
|
||||
module_account_yodlee = fields.Boolean("Bank Interface - Sync your bank feeds automatically")
|
||||
@@ -49,7 +49,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_account_bank_statement_import_ofx = fields.Boolean("Import in .ofx format")
|
||||
module_account_bank_statement_import_csv = fields.Boolean("Import in .csv format")
|
||||
module_account_bank_statement_import_camt = fields.Boolean("Import in CAMT.053 format")
|
||||
module_currency_rate_live = fields.Boolean(string="Allow Currency Rate Live")
|
||||
module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates")
|
||||
module_print_docsaway = fields.Boolean(string="Docsaway")
|
||||
module_product_margin = fields.Boolean(string="Allow Product Margin")
|
||||
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
|
||||
|
||||
@@ -13,6 +13,10 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
_name = 'report.account.report_agedpartnerbalance'
|
||||
|
||||
def _get_partner_move_lines(self, account_type, date_from, target_move, period_length):
|
||||
# This method can receive the context key 'include_nullified_amount' {Boolean}
|
||||
# Do an invoice and a payment and unreconcile. The amount will be nullified
|
||||
# By default, the partner wouldn't appear in this report.
|
||||
# The context key allow it to appear
|
||||
periods = {}
|
||||
start = datetime.strptime(date_from, "%Y-%m-%d")
|
||||
for i in range(5)[::-1]:
|
||||
@@ -190,7 +194,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
values['name'] = _('Unknown Partner')
|
||||
values['trust'] = False
|
||||
|
||||
if at_least_one_amount:
|
||||
if at_least_one_amount or self._context.get('include_nullified_amount'):
|
||||
res.append(values)
|
||||
|
||||
return res, total, lines
|
||||
|
||||
@@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel):
|
||||
"FROM account_move_line l "
|
||||
"JOIN account_account_type at ON (l.user_type_id = at.id) "
|
||||
"JOIN account_move m ON (l.move_id = m.id) "
|
||||
"WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
|
||||
"WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
|
||||
for row in self.env.cr.dictfetchall():
|
||||
res[row.pop('partner_id')].append(row)
|
||||
return res
|
||||
|
||||
@@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel):
|
||||
full_account = []
|
||||
currency = self.env['res.currency']
|
||||
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
|
||||
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
|
||||
query = """
|
||||
SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code
|
||||
@@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel):
|
||||
return
|
||||
result = 0.0
|
||||
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
|
||||
|
||||
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
|
||||
query = """SELECT sum(""" + field + """)
|
||||
@@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel):
|
||||
AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),))
|
||||
data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()]
|
||||
params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
|
||||
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
|
||||
query = """
|
||||
SELECT DISTINCT "account_move_line".partner_id
|
||||
FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
<group>
|
||||
<field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
|
||||
<field name="date_invoice" string="Bill Date"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
|
||||
<field name="move_name" invisible="1"/>
|
||||
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
@@ -299,7 +299,7 @@
|
||||
<field name="amount"/>
|
||||
<field name="amount_rounding" invisible="1"/>
|
||||
<field name="amount_total" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1" force_save="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_invoice"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
|
||||
<field name="move_name" invisible="1"/>
|
||||
<field name="user_id" groups="base.group_user"/>
|
||||
<label for="currency_id" groups="base.group_multi_currency"/>
|
||||
@@ -500,7 +500,7 @@
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="amount_rounding" invisible="1"/>
|
||||
<field name="amount_total"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1" force_save="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<field name="module_currency_rate_live" widget="upgrade_boolean"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Automatic Currency Rates"/>
|
||||
<label for="module_currency_rate_live"/>
|
||||
<div class="text-muted" id="update_currency_live">
|
||||
Update exchange rates automatically
|
||||
</div>
|
||||
@@ -190,7 +190,7 @@
|
||||
<field name="module_account_reports_followup" widget="upgrade_boolean"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Follow-up Levels"/>
|
||||
<label for="module_account_reports_followup"/>
|
||||
<div class="text-muted">
|
||||
Use follow-up levels and schedule actions
|
||||
</div>
|
||||
@@ -256,7 +256,7 @@
|
||||
<field name="module_account_sepa" widget="upgrade_boolean"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane" name="sepa_right_pane">
|
||||
<label string="SEPA Credit Transfer (SCT)" for="module_account_sepa"/>
|
||||
<label for="module_account_sepa"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
|
||||
<div class="text-muted">
|
||||
Pay your bills in one-click using Euro SEPA service
|
||||
|
||||
@@ -154,6 +154,7 @@ class AccountVoucher(models.Model):
|
||||
|
||||
@api.onchange('partner_id', 'pay_now')
|
||||
def onchange_partner_id(self):
|
||||
pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
|
||||
if self.pay_now != 'pay_now':
|
||||
if self.partner_id:
|
||||
self.account_id = self.partner_id.property_account_receivable_id \
|
||||
@@ -163,6 +164,12 @@ class AccountVoucher(models.Model):
|
||||
domain = [('deprecated', '=', False), ('internal_type', '=', account_type)]
|
||||
|
||||
self.account_id = self.env['account.account'].search(domain, limit=1)
|
||||
else:
|
||||
if self.voucher_type == 'purchase':
|
||||
pay_journal_domain.append(('outbound_payment_method_ids', '!=', False))
|
||||
else:
|
||||
pay_journal_domain.append(('inbound_payment_method_ids', '!=', False))
|
||||
return {'domain': {'payment_journal_id': pay_journal_domain}}
|
||||
|
||||
@api.multi
|
||||
def proforma_voucher(self):
|
||||
|
||||
@@ -96,3 +96,6 @@ class ResUsers(models.Model):
|
||||
internally
|
||||
"""
|
||||
return default_crypt_context
|
||||
|
||||
def _get_session_token_fields(self):
|
||||
return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'}
|
||||
|
||||
@@ -117,3 +117,6 @@ class ResUsers(models.Model):
|
||||
res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)])
|
||||
if not res:
|
||||
raise
|
||||
|
||||
def _get_session_token_fields(self):
|
||||
return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
from lxml import etree
|
||||
|
||||
from odoo import api, models, fields
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
@@ -24,14 +26,15 @@ class Partner(models.Model):
|
||||
doc = etree.fromstring(arch)
|
||||
if doc.xpath("//field[@name='city_id']"):
|
||||
return arch
|
||||
label = _('City')
|
||||
for city_node in doc.xpath("//field[@name='city']"):
|
||||
replacement_xml = """
|
||||
<div>
|
||||
<field name="country_enforce_cities" invisible="1"/>
|
||||
<field name='city' attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
|
||||
<field name='city_id' attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]" string="City"/>
|
||||
<field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
|
||||
<field name='city_id' placeholder="%s" string="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/>
|
||||
</div>
|
||||
"""
|
||||
""" % (label, label, label)
|
||||
city_id_node = etree.fromstring(replacement_xml)
|
||||
city_node.getparent().replace(city_node, city_id_node)
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
@api.multi
|
||||
def edit_external_header(self):
|
||||
if not self.external_report_layout:
|
||||
return False
|
||||
return self._prepare_report_view_action('web.external_layout_' + self.external_report_layout)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -55,14 +55,14 @@
|
||||
<field name="model_id">crm.opportunity.report</field>
|
||||
<field name="domain">[('probability', '=', 100)]</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count']}</field>
|
||||
<field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count__']}</field>
|
||||
</record>
|
||||
<record id="filter_opportunity_opportunities_won_per_team" model="ir.filters">
|
||||
<field name="name">Won by Channel</field>
|
||||
<field name="model_id">crm.opportunity.report</field>
|
||||
<field name="domain">[('probability', '=', 100)]</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected revenue']}</field>
|
||||
<field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected_revenue']}</field>
|
||||
</record>
|
||||
<record id="filter_opportunity_country" model="ir.filters">
|
||||
<field name="name">Won by Country</field>
|
||||
@@ -75,7 +75,7 @@
|
||||
<field name="name">Expected Revenue by Channel</field>
|
||||
<field name="model_id">crm.opportunity.report</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count']}</field>
|
||||
<field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count__']}</field>
|
||||
</record>
|
||||
|
||||
<!-- Opportunities by user and team Search View -->
|
||||
|
||||
@@ -251,8 +251,6 @@
|
||||
<group>
|
||||
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
|
||||
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
|
||||
<field name="email_from" invisible="1"/>
|
||||
<field name="phone" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="company_currency" invisible="1"/>
|
||||
<label for="planned_revenue"/>
|
||||
@@ -267,6 +265,20 @@
|
||||
<button string="Create & Edit" name="edit_dialog" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-default" special="cancel"/>
|
||||
</footer>
|
||||
<field name="partner_name" invisible="1"/>
|
||||
<field name="contact_name" invisible="1"/>
|
||||
<field name="title" invisible="1"/>
|
||||
<field name="street" invisible="1"/>
|
||||
<field name="street2" invisible="1"/>
|
||||
<field name="city" invisible="1"/>
|
||||
<field name="state_id" invisible="1"/>
|
||||
<field name="country_id" invisible="1"/>
|
||||
<field name="email_from" invisible="1"/>
|
||||
<field name="phone" invisible="1"/>
|
||||
<field name="mobile" invisible="1"/>
|
||||
<field name="zip" invisible="1"/>
|
||||
<field name="function" invisible="1"/>
|
||||
<field name="website" invisible="1"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<field name="event_registration_ids">
|
||||
<tree string="Registration" editable="top" create="false" delete="false">
|
||||
<field name="event_id" readonly='1' force_save="1"/>
|
||||
<field name="registration_id" readonly='1'/>
|
||||
<field name="registration_id" readonly='1' force_save="1"/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
|
||||
<field name="name"/>
|
||||
<field name="email"/>
|
||||
|
||||
@@ -126,7 +126,8 @@ class Employee(models.Model):
|
||||
], groups="hr.group_hr_user", default="male")
|
||||
marital = fields.Selection([
|
||||
('single', 'Single'),
|
||||
('married', 'Married (or similar)'),
|
||||
('married', 'Married'),
|
||||
('cohabitant', 'Legal Cohabitant'),
|
||||
('widower', 'Widower'),
|
||||
('divorced', 'Divorced')
|
||||
], string='Marital Status', groups="hr.group_hr_user", default='single')
|
||||
|
||||
@@ -13,6 +13,7 @@ var FieldOrgChart = AbstractField.extend({
|
||||
|
||||
events: {
|
||||
"click .o_employee_redirect": "_onEmployeeRedirect",
|
||||
"click .o_employee_sub_redirect": "_onEmployeeSubRedirect",
|
||||
},
|
||||
/**
|
||||
* @constructor
|
||||
|
||||
@@ -76,7 +76,7 @@ class HrSalaryRuleCategory(models.Model):
|
||||
_name = 'hr.salary.rule.category'
|
||||
_description = 'Salary Rule Category'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True)
|
||||
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
|
||||
help="Linking a salary category to its parent is used only for the reporting purpose.")
|
||||
@@ -89,7 +89,7 @@ class HrSalaryRuleCategory(models.Model):
|
||||
class HrSalaryRule(models.Model):
|
||||
_name = 'hr.salary.rule'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True,
|
||||
help="The code of salary rules can be used as reference in computation of other rules. "
|
||||
"In that case, it is case sensitive.")
|
||||
|
||||
@@ -18,7 +18,7 @@ class AccountAnalyticLine(models.Model):
|
||||
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
|
||||
|
||||
employee_id = fields.Many2one('hr.employee', "Employee")
|
||||
department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
|
||||
department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True)
|
||||
|
||||
@api.onchange('project_id')
|
||||
def onchange_project_id(self):
|
||||
@@ -28,6 +28,11 @@ class AccountAnalyticLine(models.Model):
|
||||
def _onchange_employee_id(self):
|
||||
self.user_id = self.employee_id.user_id
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_department_id(self):
|
||||
for line in self:
|
||||
line.department_id = line.employee_id.department_id
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals = self._timesheet_preprocess(vals)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import logging
|
||||
import time
|
||||
from os import listdir
|
||||
from os.path import join
|
||||
from os.path import join, isdir
|
||||
try:
|
||||
from queue import Queue, Empty
|
||||
except ImportError:
|
||||
@@ -124,7 +124,7 @@ class Scanner(Thread):
|
||||
if not evdev:
|
||||
return []
|
||||
|
||||
if not os.path.isdir(self.input_dir):
|
||||
if not isdir(self.input_dir):
|
||||
return []
|
||||
|
||||
new_devices = [device for device in listdir(self.input_dir)
|
||||
|
||||
@@ -106,10 +106,11 @@
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
|
||||
<div class="oe_group_details" style="min-height: 40px">
|
||||
<img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"
|
||||
class="oe_avatar oe_kanban_avatar_smallbox" style="float:left; margin-right: 10px;"/>
|
||||
<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right"/>
|
||||
<div class="o_kanban_image">
|
||||
<img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"/>
|
||||
</div>
|
||||
<div class="o_kanban_details">
|
||||
<h4 class="o_kanban_record_title"><field name="name"/></h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ elif salary <= 2510.47:
|
||||
<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 in ['divorced', 'single', 'widower']) 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 in ['married', 'cohabitant'] 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
|
||||
@@ -184,7 +184,7 @@ else:
|
||||
<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=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
|
||||
<field name="condition_python">result = (((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with 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 in ['divorced', 'single', 'widower']) 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 in ['married', 'cohabitant'] 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"/>
|
||||
@@ -218,7 +218,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=='married' and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field>
|
||||
<field name="condition_python">result = ((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field>
|
||||
</record>
|
||||
<record id="hr_payroll_rules_baremeIII" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll_head_pp"/>
|
||||
@@ -361,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 <= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] 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>
|
||||
@@ -372,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 <= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] 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>
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
<field name="transport_mode" widget="radio"/>
|
||||
<field name="car_atn" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}" invisible="1"/>
|
||||
<field name="transport_employer_cost" invisible="1"/>
|
||||
<label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}"/>
|
||||
<div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}">
|
||||
<label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_label"/>
|
||||
<div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_div">
|
||||
<div>
|
||||
<label for="public_transport_employee_amount" class="oe_inline"/>
|
||||
<field name="public_transport_employee_amount" class="oe_inline"/>
|
||||
@@ -72,8 +72,8 @@
|
||||
<field name="public_transport_reimbursed_amount" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
<label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}"/>
|
||||
<div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row">
|
||||
<label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}" name="others_label"/>
|
||||
<div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row" name="others_div">
|
||||
<field name="others_reimbursed_amount" class="oe_inline" nolabel="1"/>
|
||||
<span>/ month</span>
|
||||
</div>
|
||||
@@ -131,8 +131,8 @@
|
||||
<field name="resident_bool" eval="False"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='marital']" position="after">
|
||||
<field name="spouse_fiscal_status" attrs="{'invisible':[('marital','!=','married')],'required':[('marital','=','married')]}" colspan="1" help="if spouse has professionnel income or not"/>
|
||||
<field name="disabled_spouse_bool" attrs="{'invisible':[('marital','!=','married')]}" colspan="1"/>
|
||||
<field name="spouse_fiscal_status" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])], 'required': [('marital', 'in', ['married', 'cohabitant'])]}" colspan="1" help="if spouse has professionnel income or not"/>
|
||||
<field name="disabled_spouse_bool" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" colspan="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='gender']" position="after">
|
||||
<field name="disabled"/>
|
||||
@@ -148,8 +148,8 @@
|
||||
<field name="other_disabled_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='spouse_fiscal_status']" position="after">
|
||||
<field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
<field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
<field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
<field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -21,7 +21,7 @@ class HrContract(models.Model):
|
||||
@api.depends('car_id', 'new_car', 'new_car_model_id')
|
||||
def _compute_car_atn_and_costs(self):
|
||||
for contract in self:
|
||||
if contract.car_id:
|
||||
if not contract.new_car and contract.car_id:
|
||||
contract.car_atn = contract.car_id.atn
|
||||
contract.company_car_total_depreciated_cost = contract.car_id.total_depreciated_cost
|
||||
elif contract.new_car and contract.new_car_model_id:
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='transport_mode']" position="after">
|
||||
<field name="new_car" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('available_cars_amount', '>', 3)]}"/>
|
||||
<label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}"/>
|
||||
<div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}">
|
||||
<label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_label"/>
|
||||
<div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_div">
|
||||
<field name="car_id" nolabel="1" attrs="{'invisible': [('new_car', '=', True)]}"/>
|
||||
<field name="available_cars_amount" invisible="1"/>
|
||||
</div>
|
||||
|
||||
@@ -6,4 +6,24 @@ from odoo import fields, models
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_print_bank_location = fields.Boolean(string='Print bank location', default=False, help='Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR.')
|
||||
l10n_ch_isr_scan_line_left = fields.Float(string='Scan line horizontal offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_scan_line_top = fields.Float(string='Scan line vertical offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
|
||||
def _compute_l10n_ch_isr(self):
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
for company in self:
|
||||
company.l10n_ch_isr_preprinted_account = bool(get_param('l10n_ch.isr_preprinted_account', default=False))
|
||||
company.l10n_ch_isr_preprinted_bank = bool(get_param('l10n_ch.isr_preprinted_bank', default=False))
|
||||
company.l10n_ch_isr_scan_line_top = float(get_param('l10n_ch.isr_scan_line_top', default=0))
|
||||
company.l10n_ch_isr_scan_line_left = float(get_param('l10n_ch.isr_scan_line_left', default=0))
|
||||
|
||||
def _set_l10n_ch_isr(self):
|
||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||
for company in self:
|
||||
set_param("l10n_ch.isr_preprinted_account", company.l10n_ch_isr_preprinted_account)
|
||||
set_param("l10n_ch.isr_preprinted_bank", company.l10n_ch_isr_preprinted_bank)
|
||||
set_param("l10n_ch.isr_scan_line_top", company.l10n_ch_isr_scan_line_top)
|
||||
set_param("l10n_ch.isr_scan_line_left", company.l10n_ch_isr_scan_line_left)
|
||||
|
||||
@@ -7,6 +7,14 @@ from odoo import fields, models
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account',
|
||||
related="company_id.l10n_ch_isr_preprinted_account")
|
||||
l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank',
|
||||
related="company_id.l10n_ch_isr_preprinted_bank")
|
||||
l10n_ch_isr_print_bank_location = fields.Boolean(string="Print bank on ISR",
|
||||
related="company_id.l10n_ch_isr_print_bank_location",
|
||||
required=True)
|
||||
l10n_ch_isr_scan_line_left = fields.Float(string='Horizontal offset',
|
||||
related="company_id.l10n_ch_isr_scan_line_left")
|
||||
l10n_ch_isr_scan_line_top = fields.Float(string='Vertical offset',
|
||||
related="company_id.l10n_ch_isr_scan_line_top")
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
and location of the bank.-->
|
||||
<t t-if="print_bank">
|
||||
<div id="voucher-for-bank">
|
||||
<p>
|
||||
<p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
|
||||
@@ -79,12 +79,11 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="voucher-bank">
|
||||
<div id="voucher-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account">
|
||||
<!--Konto/Compte/Conto-->
|
||||
<p id="voucher-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/>
|
||||
</div>
|
||||
|
||||
<p id="voucher-currency" t-field="invoice.currency_id.name"/>
|
||||
<p id="voucher-amount_units" t-esc="split_total_amount[0]"/>
|
||||
<p id="voucher-amount_cents" t-esc="split_total_amount[1]"/>
|
||||
|
||||
@@ -103,7 +102,7 @@
|
||||
and location of the bank.-->
|
||||
<t t-if="print_bank">
|
||||
<div id="slip-for-bank">
|
||||
<p>
|
||||
<p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
|
||||
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
|
||||
@@ -122,12 +121,11 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="slip-bank">
|
||||
<div id="slip-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account">
|
||||
<!--Konto/Compte/Conto-->
|
||||
<p id="slip-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/>
|
||||
</div>
|
||||
|
||||
<p id="slip-currency" t-field="invoice.currency_id.name"/>
|
||||
<p id="slip-amount_units" t-esc="split_total_amount[0]"/>
|
||||
<p id="slip-amount_cents" t-esc="split_total_amount[1]"/>
|
||||
|
||||
@@ -141,9 +139,11 @@
|
||||
<address id="slip-by_customer_address" t-field="invoice.partner_id" t-options='{"widget": "contact", "fields": ["address","name"], "no_marker": True}' />
|
||||
</div>
|
||||
|
||||
<div id="slip-optical">
|
||||
<div id="slip-optical-line">
|
||||
<!--Optical reference-->
|
||||
<span id="slip-optical_ref_line" t-field="invoice.l10n_ch_isr_optical_line"/>
|
||||
<div t-attf-style="top: {{ invoice.company_id.l10n_ch_isr_scan_line_top }}mm; left: {{ invoice.company_id.l10n_ch_isr_scan_line_left }}mm;">
|
||||
<div t-foreach="invoice.l10n_ch_isr_optical_line" t-as="char" t-esc="char" t-attf-style="right: {{ round((char_size - char_index - 1) * 0.1, 1) }}in"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ body.l10n_ch_isr #isr {
|
||||
|
||||
overflow: hidden;
|
||||
font-family: "Work Sans", Verdana, Geneva, sans-serif;
|
||||
line-height: 1;
|
||||
font-size: 13px;
|
||||
|
||||
p, address {
|
||||
@@ -58,25 +59,26 @@ body.l10n_ch_isr #isr {
|
||||
#voucher > * {
|
||||
position: absolute;
|
||||
/* default left margin for fields content */
|
||||
left: 1/10in;
|
||||
left: 3/20in;
|
||||
|
||||
&#voucher-for-contact {
|
||||
top: 3/10in;
|
||||
top: 0.25in;
|
||||
}
|
||||
|
||||
&#voucher-for-bank {
|
||||
top: 3/20in;
|
||||
top: 0.18in;
|
||||
}
|
||||
|
||||
&#voucher-for-bank + #voucher-for-contact {
|
||||
top: 4/6in;
|
||||
top: 0.68in;
|
||||
}
|
||||
|
||||
&#voucher-bank {
|
||||
top: 9/6in;
|
||||
top: 1.51in;
|
||||
left: 11/10in;
|
||||
}
|
||||
|
||||
/* TODO: remove in master */
|
||||
&#voucher-currency {
|
||||
top: 9/6 + 3/20in;
|
||||
|
||||
@@ -86,24 +88,22 @@ body.l10n_ch_isr #isr {
|
||||
|
||||
&#voucher-amount_units {
|
||||
width: 15/10in;
|
||||
top: 11/6in;
|
||||
top: 1.84in;
|
||||
|
||||
text-align: right;
|
||||
line-height: 1/6in;
|
||||
font-weight: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&#voucher-amount_cents {
|
||||
width: 4/10in;
|
||||
top: 11/6in;
|
||||
left: 19/10in;
|
||||
top: 1.84in;
|
||||
left: 20/10in;
|
||||
|
||||
line-height: 1/6in;
|
||||
font-weight: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&#voucher-by {
|
||||
top: 26/12in;
|
||||
top: 2.18in;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,25 +121,26 @@ body.l10n_ch_isr #isr {
|
||||
#slip > * {
|
||||
position: absolute;
|
||||
/* default left margin for fields content */
|
||||
left: 1/10in;
|
||||
left: 3/20in;
|
||||
|
||||
&#slip-for-contact {
|
||||
top: 3/10in;
|
||||
top: 0.25in;
|
||||
}
|
||||
|
||||
&#slip-for-bank {
|
||||
top: 3/20in;
|
||||
top: 0.18in;
|
||||
}
|
||||
|
||||
&#slip-for-bank + #slip-for-contact {
|
||||
top: 4/6in;
|
||||
top: 0.68in;
|
||||
}
|
||||
|
||||
&#slip-bank {
|
||||
top: 9/6in;
|
||||
top: 1.51in;
|
||||
left: 11/10in;
|
||||
}
|
||||
|
||||
/* TODO: remove in master */
|
||||
&#slip-currency {
|
||||
top: 9/6 + 3/20in;
|
||||
|
||||
@@ -149,34 +150,52 @@ body.l10n_ch_isr #isr {
|
||||
|
||||
&#slip-amount_units {
|
||||
width: 15/10in;
|
||||
top: 11/6in;
|
||||
top: 1.84in;
|
||||
|
||||
text-align: right;
|
||||
line-height: 1/6in;
|
||||
font-weight: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&#slip-amount_cents {
|
||||
width: 4/10in;
|
||||
top: 11/6in;
|
||||
left: 19/10in;
|
||||
top: 1.84in;
|
||||
left: 20/10in;
|
||||
|
||||
line-height: 1/6in;
|
||||
font-weight: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&#slip-reference {
|
||||
width: 3 + 3/10in;
|
||||
top: 7/6in;
|
||||
left: 2 + 5/10in;
|
||||
width: 3.3in;
|
||||
top: 1.17in;
|
||||
left: 2.5in;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&#slip-by {
|
||||
top: 11/6in;
|
||||
left: 2 + 5/10in;
|
||||
top: 1.85in;
|
||||
left: 2.55in;
|
||||
}
|
||||
|
||||
&#slip-optical-line {
|
||||
right: 3/10in;
|
||||
bottom: 4/6in;
|
||||
|
||||
/* Positioning is set for OCRB of Tsukurimashou Font Family */
|
||||
font-family: OCRB, Courier, monospace;
|
||||
font-size: 10pt;
|
||||
line-height: 0.1in;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: remove in master */
|
||||
&#slip-optical {
|
||||
right: 3/10in;
|
||||
bottom: 4/6in;
|
||||
|
||||
@@ -17,10 +17,39 @@
|
||||
Print the coordinates of your bank under the 'Payment for' title of the ISR.
|
||||
Your address will be moved to the 'in favour of' section.
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('l10n_ch_isr_print_bank_location', '=', False)]}">
|
||||
<div class="row mt16">
|
||||
<label for="l10n_ch_isr_preprinted_bank" class="col-md-4 o_light_label"/>
|
||||
<field name="l10n_ch_isr_preprinted_bank"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="l10n_ch_isr_preprinted_account" class="col-md-4 o_light_label"/>
|
||||
<field name="l10n_ch_isr_preprinted_account"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="l10n_ch-isr_print_scanline_offset">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="ISR scan line offset"/>
|
||||
<div class="text-muted">
|
||||
Offset to move the the scan line in mm
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="l10n_ch_isr_scan_line_top" class="col-md-4 o_light_label"/>
|
||||
<field name="l10n_ch_isr_scan_line_top"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="l10n_ch_isr_scan_line_left" class="col-md-4 o_light_label"/>
|
||||
<field name="l10n_ch_isr_scan_line_left"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -220,7 +220,7 @@ class AccountFrFec(models.TransientModel):
|
||||
replace(MIN(aa.name), '|', '/') AS CompteLib,
|
||||
CASE WHEN rp.ref IS null OR rp.ref = ''
|
||||
THEN COALESCE('ID ' || rp.id, '')
|
||||
ELSE rp.ref
|
||||
ELSE replace(rp.ref, '|', '/')
|
||||
END
|
||||
AS CompAuxNum,
|
||||
COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
|
||||
@@ -278,7 +278,7 @@ class AccountFrFec(models.TransientModel):
|
||||
replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib,
|
||||
CASE WHEN rp.ref IS null OR rp.ref = ''
|
||||
THEN COALESCE('ID ' || rp.id, '')
|
||||
ELSE rp.ref
|
||||
ELSE replace(rp.ref, '|', '/')
|
||||
END
|
||||
AS CompAuxNum,
|
||||
COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib,
|
||||
|
||||
@@ -37,11 +37,11 @@ class AccountChartTemplate(models.Model):
|
||||
'type': 'model',
|
||||
'res_id': out_ids[counter].id,
|
||||
'lang': lang,
|
||||
'src': element.name,
|
||||
'src': element[in_field],
|
||||
'value': value[element.id],
|
||||
})
|
||||
else:
|
||||
_logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element.name))
|
||||
_logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element[in_field]))
|
||||
counter += 1
|
||||
return True
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ var Thread = Widget.extend({
|
||||
} else {
|
||||
this.trigger('redirect', options.model, options.id);
|
||||
}
|
||||
}, 200, true),
|
||||
}, 500, true),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
|
||||
- <small class="o_mail_timestamp" t-att-title="message.date.format(date_format)"><t t-esc="message.hour"/></small>
|
||||
<t t-if="message.model and (message.model != 'mail.channel') and options.display_document_link">
|
||||
<a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link">on <t t-esc="message.record_name"/></a>
|
||||
on <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link"><t t-esc="message.record_name"/></a>
|
||||
</t>
|
||||
<t t-if="message.origin_id and (message.origin_id !== options.channel_id)">
|
||||
(from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
|
||||
|
||||
@@ -51,7 +51,7 @@ class Partner(models.Model):
|
||||
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
|
||||
'member_lines.account_invoice_line.invoice_id.payment_ids',
|
||||
'free_member',
|
||||
'member_lines.date_to', 'member_lines.date_from',
|
||||
'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
|
||||
'membership_state',
|
||||
'associate_member.membership_state')
|
||||
def _compute_membership_start(self):
|
||||
@@ -65,7 +65,7 @@ class Partner(models.Model):
|
||||
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
|
||||
'member_lines.account_invoice_line.invoice_id.payment_ids',
|
||||
'free_member',
|
||||
'member_lines.date_to', 'member_lines.date_from',
|
||||
'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
|
||||
'membership_state',
|
||||
'associate_member.membership_state')
|
||||
def _compute_membership_stop(self):
|
||||
@@ -79,7 +79,7 @@ class Partner(models.Model):
|
||||
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
|
||||
'member_lines.account_invoice_line.invoice_id.payment_ids',
|
||||
'free_member',
|
||||
'member_lines.date_to', 'member_lines.date_from',
|
||||
'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
|
||||
'membership_state',
|
||||
'associate_member.membership_state')
|
||||
def _compute_membership_cancel(self):
|
||||
|
||||
@@ -56,14 +56,17 @@ class ProductProduct(models.Model):
|
||||
mo_count = fields.Integer('# Manufacturing Orders', compute='_compute_mo_count')
|
||||
|
||||
def _compute_bom_count(self):
|
||||
# read_group_res: BOM where product_id is set
|
||||
# read_group_res_tmpl: BOM where product_tmpl_id is set and product_id is not set
|
||||
# The total count is the sum of both.
|
||||
read_group_res = self.env['mrp.bom'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id'])
|
||||
mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res])
|
||||
read_group_res_tmpl = self.env['mrp.bom'].read_group([
|
||||
('product_tmpl_id', 'in', self.mapped('product_tmpl_id.id')), ('product_id', '=', False)
|
||||
], ['product_tmpl_id'], ['product_tmpl_id'])
|
||||
mapped_data_tmpl = dict([(data['product_tmpl_id'][0], data['product_tmpl_id_count']) for data in read_group_res_tmpl])
|
||||
for product in self:
|
||||
if product.product_tmpl_id.product_variant_count == 1:
|
||||
bom_count = mapped_data.get(product.id, product.product_tmpl_id.bom_count)
|
||||
else:
|
||||
bom_count = mapped_data.get(product.id, 0)
|
||||
product.bom_count = bom_count
|
||||
product.bom_count = mapped_data.get(product.id, 0) + mapped_data_tmpl.get(product.product_tmpl_id.id, 0)
|
||||
|
||||
@api.multi
|
||||
def _compute_used_in_bom_count(self):
|
||||
|
||||
@@ -313,6 +313,7 @@ class PaymentAcquirer(models.Model):
|
||||
'billing_partner': billing_partner,
|
||||
'billing_partner_id': billing_partner_id,
|
||||
'billing_partner_name': billing_partner.name,
|
||||
'billing_partner_commercial_company_name': billing_partner.commercial_company_name,
|
||||
'billing_partner_lang': billing_partner.lang,
|
||||
'billing_partner_email': billing_partner.email,
|
||||
'billing_partner_zip': billing_partner.zip,
|
||||
@@ -845,16 +846,13 @@ class PaymentToken(models.Model):
|
||||
'partner_country_id': self.partner_id.country_id.id,
|
||||
})
|
||||
|
||||
try:
|
||||
kwargs.update({'3d_secure': True})
|
||||
tx.s2s_do_transaction(**kwargs)
|
||||
# if 3D secure is called, then we do not refund right now
|
||||
if tx.html_3ds:
|
||||
return tx
|
||||
except:
|
||||
_logger.error('Error while validating a payment method')
|
||||
finally:
|
||||
kwargs.update({'3d_secure': True})
|
||||
tx.s2s_do_transaction(**kwargs)
|
||||
|
||||
# if 3D secure is called, then we do not refund right now
|
||||
if not tx.html_3ds:
|
||||
tx.s2s_do_refund()
|
||||
|
||||
return tx
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -106,6 +106,7 @@ odoo.define('payment.payment_form', function (require) {
|
||||
else {
|
||||
checked_radio.value = data.id; // set the radio value to the new card id
|
||||
form.submit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if the server has returned false, we display an error
|
||||
|
||||
@@ -25,6 +25,7 @@ class PaymentAcquirerCommon(common.TransactionCase):
|
||||
'partner_country_id': self.country_belgium.id,
|
||||
'partner_country_name': 'Belgium',
|
||||
'billing_partner_name': 'Norbert Buyer',
|
||||
'billing_partner_commercial_company_name': 'Big Company',
|
||||
'billing_partner_lang': 'en_US',
|
||||
'billing_partner_email': 'norbert.buyer@example.com',
|
||||
'billing_partner_address': 'Huge Street 2/543',
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
|
||||
<t t-set="id" t-value="acq.id"/>
|
||||
<t t-set="partner_id" t-value="partner_id"/>
|
||||
<t t-set="return_url" t-value="''"/>
|
||||
<t t-if="not return_url" t-set="return_url" t-value="''"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ class AcquirerAdyen(models.Model):
|
||||
]
|
||||
|
||||
hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii'))
|
||||
raw_values = {k: values.get(k.encode('ascii'), '') for k in keys if k in values}
|
||||
raw_values = {k: values.get(k, '') for k in keys if k in values}
|
||||
raw_values_ordered = OrderedDict(sorted(raw_values.items(), key=lambda t: t[0]))
|
||||
|
||||
return signParams(raw_values_ordered)
|
||||
@@ -151,8 +151,8 @@ class AcquirerAdyen(models.Model):
|
||||
'sessionValidity': tmp_date,
|
||||
'resURL': urls.url_join(base_url, AdyenController._return_url),
|
||||
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False,
|
||||
'merchantSig': self._adyen_generate_merchant_sig('in', values),
|
||||
})
|
||||
values['merchantSig'] = self._adyen_generate_merchant_sig('in', values)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
@@ -127,8 +127,18 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
for field_name in mandatory_fields:
|
||||
if not data.get(field_name):
|
||||
error[field_name] = 'missing'
|
||||
if data['cc_expiry'] and datetime.now().strftime('%y%m') > datetime.strptime(data['cc_expiry'], '%m / %y').strftime('%y%m'):
|
||||
return False
|
||||
if data['cc_expiry']:
|
||||
# FIX we split the date into their components and check if there is two components containing only digits
|
||||
# this fixes multiples crashes, if there was no space between the '/' and the components the code was crashing
|
||||
# the code was also crashing if the customer was proving non digits to the date.
|
||||
cc_expiry = [i.strip() for i in data['cc_expiry'].split('/')]
|
||||
if len(cc_expiry) != 2 or any(not i.isdigit() for i in cc_expiry):
|
||||
return False
|
||||
try:
|
||||
if datetime.now().strftime('%y%m') > datetime.strptime('/'.join(cc_expiry), '%m/%y').strftime('%y%m'):
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return False if error else True
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -63,6 +63,7 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
'x_invoice_num': 'SO004',
|
||||
'x_first_name': 'Norbert',
|
||||
'x_last_name': 'Buyer',
|
||||
'x_company': 'Big Company',
|
||||
'x_address': 'Huge Street 2/543',
|
||||
'x_city': 'Sin City',
|
||||
'x_zip': '1000',
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<!-- Billing Information-->
|
||||
<input type='hidden' name='x_first_name' t-att-value="billing_first_name"/>
|
||||
<input type='hidden' name='x_last_name' t-att-value="billing_last_name"/>
|
||||
<input type="hidden" name="x_company" t-att-value="billing_partner_commercial_company_name"/>
|
||||
<input type='hidden' name='x_address' t-att-value="billing_address"/>
|
||||
<input type='hidden' name='x_city' t-att-value="billing_city"/>
|
||||
<input type='hidden' name='x_zip' t-att-value="billing_zip_code"/>
|
||||
|
||||
@@ -214,8 +214,8 @@ class PaymentTxOgone(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
# ogone status
|
||||
_ogone_valid_tx_status = [5, 9, 8]
|
||||
_ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99, 81, 82]
|
||||
_ogone_pending_tx_status = [46] # 3DS HTML response
|
||||
_ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
|
||||
_ogone_pending_tx_status = [46, 81, 82] # 46 = 3DS HTML response
|
||||
_ogone_cancel_tx_status = [1]
|
||||
|
||||
# --------------------------------------------------
|
||||
@@ -283,7 +283,7 @@ class PaymentTxOgone(models.Model):
|
||||
return invalid_parameters
|
||||
|
||||
def _ogone_form_validate(self, data):
|
||||
if self.state in ['done', 'refunded']:
|
||||
if self.state in ['done', 'refunding', 'refunded']:
|
||||
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
|
||||
return True
|
||||
|
||||
@@ -466,6 +466,9 @@ class PaymentTxOgone(models.Model):
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
# if this transaction is a validation one, then we refund the money we just withdrawn
|
||||
if self.type == 'validation':
|
||||
self.s2s_do_refund()
|
||||
return True
|
||||
elif status in self._ogone_cancel_tx_status:
|
||||
self.write({
|
||||
@@ -474,11 +477,13 @@ class PaymentTxOgone(models.Model):
|
||||
})
|
||||
elif status in self._ogone_pending_tx_status:
|
||||
new_state = 'refunding' if self.state == 'refunding' else 'pending'
|
||||
self.write({
|
||||
vals = {
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
'html_3ds': ustr(base64.b64decode(tree.HTML_ANSWER.text)),
|
||||
})
|
||||
}
|
||||
if status == 46: # HTML 3DS
|
||||
vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text))
|
||||
self.write(vals)
|
||||
elif status in self._ogone_wait_tx_status and tries > 0:
|
||||
time.sleep(0.5)
|
||||
self.write({'acquirer_reference': tree.get('PAYID')})
|
||||
|
||||
@@ -194,7 +194,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
},
|
||||
},{
|
||||
model: 'res.country',
|
||||
fields: ['name'],
|
||||
fields: ['name', 'vat_label'],
|
||||
loaded: function(self,countries){
|
||||
self.countries = countries;
|
||||
self.company.country = null;
|
||||
@@ -2119,6 +2119,7 @@ exports.Order = Backbone.Model.extend({
|
||||
company_registry: company.company_registry,
|
||||
contact_address: company.partner_id[1],
|
||||
vat: company.vat,
|
||||
vat_label: company.country.vat_label,
|
||||
name: company.name,
|
||||
phone: company.phone,
|
||||
logo: this.pos.company_logo_base64,
|
||||
@@ -2263,7 +2264,9 @@ exports.Order = Backbone.Model.extend({
|
||||
}
|
||||
})
|
||||
|
||||
unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded;
|
||||
if (mapped_included_taxes.length > 0) {
|
||||
unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded;
|
||||
}
|
||||
|
||||
line.set_unit_price(unit_price);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ from werkzeug.exceptions import NotFound, Forbidden
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import consteq
|
||||
from odoo.tools import consteq, plaintext2html
|
||||
|
||||
|
||||
def _has_token_access(res_model, res_id, token=''):
|
||||
@@ -59,6 +59,8 @@ class PortalChatter(http.Controller):
|
||||
def portal_chatter_post(self, res_model, res_id, message, **kw):
|
||||
url = request.httprequest.referrer
|
||||
if message:
|
||||
# message is received in plaintext and saved in html
|
||||
message = plaintext2html(message)
|
||||
_message_post_helper(res_model, int(res_id), message, **kw)
|
||||
url = url + "#discussion"
|
||||
return request.redirect(url)
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
the message will be posted with the identity of the partner_id of the object
|
||||
-->
|
||||
<template id="message_thread">
|
||||
<div id="discussion" class="hidden-print o_portal_chatter"
|
||||
<div id="discussion" class="hidden-print o_portal_chatter o_not_editable"
|
||||
t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div>Tel:<t t-esc='receipt.company.phone' /></div>
|
||||
</t>
|
||||
<t t-if='receipt.company.vat'>
|
||||
<div><t t-esc="receipt.company.country_id.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div>
|
||||
<div><t t-esc="receipt.company.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div>
|
||||
</t>
|
||||
<t t-if='receipt.company.email'>
|
||||
<div><t t-esc='receipt.company.email' /></div>
|
||||
|
||||
@@ -119,6 +119,7 @@ class ProductTemplate(models.Model):
|
||||
active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.")
|
||||
color = fields.Integer('Color Index')
|
||||
|
||||
is_product_variant = fields.Boolean(string='Is a product variant', compute='_compute_is_product_variant')
|
||||
attribute_line_ids = fields.One2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes')
|
||||
product_variant_ids = fields.One2many('product.product', 'product_tmpl_id', 'Products', required=True)
|
||||
# performance: product_variant_id provides prefetching on the first product variant only
|
||||
@@ -236,6 +237,13 @@ class ProductTemplate(models.Model):
|
||||
for template in (self - unique_variants):
|
||||
template.weight = 0.0
|
||||
|
||||
def _compute_is_product_variant(self):
|
||||
for template in self:
|
||||
if template._name == 'product.template':
|
||||
template.is_product_variant = False
|
||||
else:
|
||||
template.is_product_variant = True
|
||||
|
||||
@api.model
|
||||
def _get_weight_uom_id_from_ir_config_parameter(self):
|
||||
""" Get the unit of measure to interpret the `weight` field. By default, we considerer
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</header>
|
||||
<sheet>
|
||||
<field name='product_variant_count' invisible='1'/>
|
||||
<field name='is_product_variant' invisible='1'/>
|
||||
<field name="id" invisible="True"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object"
|
||||
@@ -48,8 +49,8 @@
|
||||
</group>
|
||||
<group name="group_standard_price">
|
||||
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
|
||||
<label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1)]}"/>
|
||||
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1)]}">
|
||||
<label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"/>
|
||||
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}">
|
||||
<field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
</div>
|
||||
<field name="company_id" groups="base.group_multi_company"
|
||||
@@ -340,7 +341,8 @@
|
||||
</field>
|
||||
</field>
|
||||
<xpath expr="//div[hasclass('oe_title')]" position="inside">
|
||||
<field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" readonly="1"
|
||||
groups="product.group_product_variant"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -37,9 +37,7 @@ class ProductProduct(models.Model):
|
||||
price = 0.0
|
||||
result, result2 = bom.explode(self, 1)
|
||||
for sbom, sbom_data in result2:
|
||||
if not sbom.attribute_value_ids:
|
||||
# No attribute_value_ids means the bom line is not variant specific
|
||||
price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
|
||||
price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
|
||||
if bom.routing_id:
|
||||
# FIXME master: remove me
|
||||
if hasattr(self.env['mrp.workcenter'], 'costs_hour'):
|
||||
|
||||
@@ -35,4 +35,24 @@
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_view_form_easy_inherit_extended" model="ir.ui.view">
|
||||
<field name="name">product.product.view.form.easy.inherit.extended</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
|
||||
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="standard_price" position="after">
|
||||
<field name="cost_method" invisible='1'/>
|
||||
<field name="bom_count" invisible='1'/>
|
||||
<button name="compute_price"
|
||||
string="Compute from BOM" type="object"
|
||||
attrs="{'invisible': ['|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0)]}"
|
||||
help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only."
|
||||
class="oe_link oe_read_only"
|
||||
colspan="2"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -33,8 +33,7 @@ class TestTimesheetHolidays(TestTimesheet):
|
||||
# leave dates : from next monday to next wednesday (to avoid crashing tests on weekend, when
|
||||
# there is no work days in working calendar)
|
||||
# NOTE: second and millisecond can add a working days
|
||||
today = datetime.combine(datetime.today(), time.min)
|
||||
self.leave_start_datetime = today.replace(hour=7) + relativedelta(weeks=0, days=1, weekday=0)
|
||||
self.leave_start_datetime = datetime(2018, 2, 5, 7, 0, 0, 0) # this is monday
|
||||
self.leave_end_datetime = self.leave_start_datetime + relativedelta(days=3)
|
||||
|
||||
# all company have those internal project/task (created by default)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html"><![CDATA[
|
||||
<p>Dear
|
||||
% if object.partner_id.is_company and object.child_ids:
|
||||
% if object.partner_id.is_company and object.partner_id.child_ids:
|
||||
${object.partner_id.child_ids[0].name}
|
||||
% else :
|
||||
${object.partner_id.name}
|
||||
@@ -55,7 +55,7 @@ from ${object.company_id.name}.
|
||||
<field name="report_name">PO_${(object.name or '').replace('/','_')}</field>
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html"><![CDATA[
|
||||
<p>Dear ${object.partner_id.name}
|
||||
<p>Dear ${object.partner_id.name}
|
||||
% if object.partner_id.parent_id:
|
||||
(<i>${object.partner_id.parent_id.name}</i>)
|
||||
% endif
|
||||
|
||||
@@ -11,17 +11,6 @@ class StockPicking(models.Model):
|
||||
purchase_id = fields.Many2one('purchase.order', related='move_lines.purchase_line_id.order_id',
|
||||
string="Purchase Orders", readonly=True)
|
||||
|
||||
@api.model
|
||||
def _create_backorder(self, backorder_moves=[]):
|
||||
res = super(StockPicking, self)._create_backorder(backorder_moves)
|
||||
for picking in self:
|
||||
if picking.picking_type_id.code == 'incoming':
|
||||
for backorder in self.search([('backorder_id', '=', picking.id)]):
|
||||
backorder.message_post_with_view('mail.message_origin_link',
|
||||
values={'self': backorder, 'origin': backorder.purchase_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return res
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
@@ -16,8 +16,6 @@ def uninstall_hook(cr, registry):
|
||||
db_registry = odoo.modules.registry.Registry.new(dbname)
|
||||
with api.Environment.manage(), db_registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
if 'account.invoice.report' in env:
|
||||
env['account.invoice.report'].init()
|
||||
if 'crm.team' in env:
|
||||
recs = env['crm.team'].search([])
|
||||
for rec in recs:
|
||||
|
||||
@@ -575,7 +575,7 @@ class SaleOrder(models.Model):
|
||||
report_pages.append([])
|
||||
# Append category to current report page
|
||||
report_pages[-1].append({
|
||||
'name': category and category.name or 'Uncategorized',
|
||||
'name': category and category.name or _('Uncategorized'),
|
||||
'subtotal': category and category.subtotal,
|
||||
'pagebreak': category and category.pagebreak,
|
||||
'lines': list(lines)
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<span class="label label-danger"><i class="fa fa-fw fa-warning"></i><span class="hidden-xs"> Problem</span></span>
|
||||
</t>
|
||||
<t t-if="order.state == 'done'">
|
||||
<span class="label label-default hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span>
|
||||
<span class="label label-success hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span>
|
||||
</t>
|
||||
</td>
|
||||
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
|
||||
@@ -148,7 +148,7 @@
|
||||
<span class="label label-default"><i class="fa fa-fw fa-remove"/> Cancelled</span>
|
||||
</t>
|
||||
<t t-if="order.state == 'done'">
|
||||
<span class="label label-default"><i class="fa fa-fw fa-remove"/> Done</span>
|
||||
<span class="label label-success"><i class="fa fa-fw fa-check" /> Done</span>
|
||||
</t>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from functools import partial
|
||||
import openerp
|
||||
from openerp import api, SUPERUSER_ID
|
||||
|
||||
from . import models # noqa
|
||||
from . import report # noqa
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
def recreate_view(dbname):
|
||||
db_registry = openerp.modules.registry.Registry.new(dbname)
|
||||
with api.Environment.manage(), db_registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
if 'sale.report' in env:
|
||||
env['sale.report'].init()
|
||||
|
||||
cr.after("commit", partial(recreate_view, cr.dbname))
|
||||
|
||||
@@ -15,5 +15,4 @@ Price and Cost Price.
|
||||
'depends':['sale_management'],
|
||||
'demo':['data/sale_margin_demo.xml'],
|
||||
'data':['security/ir.model.access.csv','views/sale_margin_view.xml'],
|
||||
'uninstall_hook': "uninstall_hook",
|
||||
}
|
||||
|
||||
@@ -50,17 +50,6 @@ class StockPicking(models.Model):
|
||||
|
||||
sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True)
|
||||
|
||||
@api.multi
|
||||
def _create_backorder(self, backorder_moves=[]):
|
||||
res = super(StockPicking, self)._create_backorder(backorder_moves)
|
||||
for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
|
||||
backorder = picking.search([('backorder_id', '=', picking.id)])
|
||||
if backorder.sale_id:
|
||||
backorder.message_post_with_view(
|
||||
'mail.message_origin_link',
|
||||
values={'self': backorder, 'origin': backorder.sale_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return res
|
||||
|
||||
def _log_less_quantities_than_expected(self, moves):
|
||||
""" Log an activity on sale order that are linked to moves. The
|
||||
@@ -102,4 +91,4 @@ class StockPicking(models.Model):
|
||||
documents = self._log_activity_get_documents(moves, 'sale_line_id', 'DOWN', _keys_in_sorted, _keys_in_groupby)
|
||||
self._log_activity(_render_note_exception_quantity, documents)
|
||||
|
||||
return super(StockPicking, self)._log_less_quantities_than_expected(moves)
|
||||
return super(StockPicking, self)._log_less_quantities_than_expected(moves)
|
||||
@@ -120,7 +120,6 @@
|
||||
|
||||
|
||||
<record id="warehouse0" model="stock.warehouse">
|
||||
<field model="res.company" name="name" search="[]" use="name"/>
|
||||
<field name="partner_id" ref="base.main_partner"/>
|
||||
<field name="code">WH</field>
|
||||
</record>
|
||||
|
||||
@@ -1068,18 +1068,8 @@ class StockMove(models.Model):
|
||||
return moves_todo
|
||||
|
||||
if picking:
|
||||
moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
if moves_to_backorder:
|
||||
backorder_picking = picking.copy({
|
||||
'name': '/',
|
||||
'move_lines': [],
|
||||
'move_line_ids': [],
|
||||
'backorder_id': picking.id
|
||||
})
|
||||
picking.message_post(body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (backorder_picking.id, backorder_picking.name))
|
||||
moves_to_backorder.write({'picking_id': backorder_picking.id})
|
||||
moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
|
||||
moves_to_backorder._action_assign()
|
||||
picking._create_backorder()
|
||||
|
||||
return moves_todo
|
||||
|
||||
def unlink(self):
|
||||
|
||||
@@ -378,9 +378,9 @@ class Picking(models.Model):
|
||||
@api.depends('move_lines.date_expected')
|
||||
def _compute_scheduled_date(self):
|
||||
if self.move_type == 'direct':
|
||||
self.scheduled_date = min(self.move_lines.mapped('date_expected') or [False])
|
||||
self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
|
||||
else:
|
||||
self.scheduled_date = max(self.move_lines.mapped('date_expected') or [False])
|
||||
self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
|
||||
|
||||
@api.one
|
||||
def _set_scheduled_date(self):
|
||||
@@ -813,31 +813,25 @@ class Picking(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _create_backorder(self, backorder_moves=[]):
|
||||
""" Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
|
||||
""" Move all non-done lines into a new backorder picking.
|
||||
"""
|
||||
# TDE note: o2o conversion, todo multi
|
||||
backorders = self.env['stock.picking']
|
||||
for picking in self:
|
||||
backorder_moves = backorder_moves or picking.move_lines
|
||||
if self._context.get('do_only_split'):
|
||||
not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', []))
|
||||
else:
|
||||
not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel'))
|
||||
if not not_done_bo_moves:
|
||||
continue
|
||||
backorder_picking = picking.copy({
|
||||
'name': '/',
|
||||
'move_lines': [],
|
||||
'move_line_ids': [],
|
||||
'backorder_id': picking.id
|
||||
})
|
||||
picking.message_post(body=_("Back order <em>%s</em> <b>created</b>.") % (backorder_picking.name))
|
||||
not_done_bo_moves.write({'picking_id': backorder_picking.id})
|
||||
if not picking.date_done:
|
||||
picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
backorder_picking.action_confirm()
|
||||
backorder_picking.action_assign()
|
||||
backorders |= backorder_picking
|
||||
moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
if moves_to_backorder:
|
||||
backorder_picking = picking.copy({
|
||||
'name': '/',
|
||||
'move_lines': [],
|
||||
'move_line_ids': [],
|
||||
'backorder_id': picking.id
|
||||
})
|
||||
picking.message_post(
|
||||
body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (
|
||||
backorder_picking.id, backorder_picking.name))
|
||||
moves_to_backorder.write({'picking_id': backorder_picking.id})
|
||||
moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
|
||||
backorder_picking.action_assign()
|
||||
backorders |= backorder_picking
|
||||
return backorders
|
||||
|
||||
def _log_activity_get_documents(self, orig_obj_changes, stream_field, stream, sorted_method=False, groupby_method=False):
|
||||
|
||||
@@ -21,7 +21,7 @@ class Warehouse(models.Model):
|
||||
# namedtuple used in helper methods generating values for routes
|
||||
Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type'])
|
||||
|
||||
name = fields.Char('Warehouse Name', index=True, required=True)
|
||||
name = fields.Char('Warehouse Name', index=True, required=True, default=lambda self: self.env['res.company']._company_default_get('stock.inventory').name)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'),
|
||||
@@ -684,11 +684,11 @@ class Warehouse(models.Model):
|
||||
|
||||
def _get_sequence_values(self):
|
||||
return {
|
||||
'in_type_id': {'name': self.name + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
|
||||
'out_type_id': {'name': self.name + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
|
||||
'pack_type_id': {'name': self.name + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
|
||||
'pick_type_id': {'name': self.name + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
|
||||
'int_type_id': {'name': self.name + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
|
||||
'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
|
||||
'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
|
||||
'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
|
||||
'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
|
||||
'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
|
||||
}
|
||||
|
||||
def _format_rulename(self, from_loc, dest_loc, suffix):
|
||||
|
||||
@@ -53,20 +53,19 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Product</strong></th>
|
||||
<th><strong>Ordered Quantity</strong></th>
|
||||
<th><strong>Quantity</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="o.move_lines" t-as="move">
|
||||
<td><span t-field="move.product_id"/></td>
|
||||
<td>
|
||||
<span t-field="move.ordered_qty"/>
|
||||
<span t-field="move.quantity_done"/>
|
||||
<span t-field="move.product_uom"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<t t-set="backorder" t-value="False"/>
|
||||
<table class="table table-condensed mt48" t-if="o.move_line_ids">
|
||||
<t t-set="has_serial_number" t-value="o.move_line_ids.mapped('lot_id')" groups="stock.group_production_lot"/>
|
||||
<thead>
|
||||
@@ -75,10 +74,7 @@
|
||||
<th name="lot_serial" t-if="has_serial_number">
|
||||
Lot/Serial Number
|
||||
</th>
|
||||
<th class="text-center"><strong>Ordered Quantity</strong></th>
|
||||
<th t-if="any([move_line.state == 'done' for move_line in o.move_line_ids])" class="text-right">
|
||||
<strong>Delivered Quantity</strong>
|
||||
</th>
|
||||
<th class="text-center"><strong>Quantity</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -112,13 +108,6 @@
|
||||
</td>
|
||||
</t>
|
||||
<td class="text-center">
|
||||
<span t-field="move_line.ordered_qty"/>
|
||||
<span t-field="move_line.product_uom_id"/>
|
||||
</td>
|
||||
<td class="text-right" t-if="move_line.state == 'done'">
|
||||
<t t-if="move_line.ordered_qty != move_line.qty_done">
|
||||
<t t-set="backorder" t-value="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"/>
|
||||
</t>
|
||||
<span t-field="move_line.qty_done"/>
|
||||
<span t-field="move_line.product_uom_id"/>
|
||||
</td>
|
||||
@@ -129,7 +118,7 @@
|
||||
This shipment is a backorder of <t t-esc="o.backorder_id.name"/>.
|
||||
</p>
|
||||
<p>
|
||||
<t t-if="backorder">
|
||||
<t t-if="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])">
|
||||
All items couldn't be shipped, the remaining ones will be shipped as soon as they become available.
|
||||
</t>
|
||||
</p>
|
||||
|
||||
@@ -127,13 +127,13 @@
|
||||
<span t-field="ml.lot_id"/>
|
||||
<span t-if="not ml.lot_id" t-field="ml.lot_name"/>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<span t-field="ml.qty_done"/>
|
||||
<t t-if="o.state!='done'">
|
||||
/ <span t-field="ml.product_uom_qty"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-xs-8">
|
||||
<span t-field="ml.qty_done"/>
|
||||
<t t-if="o.state!='done'">
|
||||
/ <span t-field="ml.product_uom_qty"/>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
<td width="30%" align="right">
|
||||
<div>
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
<field name="priority"/>
|
||||
<field name="state"/>
|
||||
<templates>
|
||||
|
||||
@@ -94,7 +94,7 @@ class ProductChangeQuantity(models.TransientModel):
|
||||
else:
|
||||
inventory_filter = 'none'
|
||||
inventory = Inventory.create({
|
||||
'name': _('INV: %s') % tools.ustr(wizard.product_id.name),
|
||||
'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name),
|
||||
'filter': inventory_filter,
|
||||
'product_id': wizard.product_id.id,
|
||||
'location_id': wizard.location_id.id,
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_round, pycompat
|
||||
from odoo.tools import float_compare, float_round, float_is_zero, pycompat
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -353,8 +353,11 @@ class StockMove(models.Model):
|
||||
std_price_update = {}
|
||||
for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
if product_tot_qty_available == 0:
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
else:
|
||||
# Get the standard price
|
||||
|
||||
@@ -21,7 +21,7 @@ class LandedCost(models.Model):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
name = fields.Char(
|
||||
'Name', default=lambda self: self.env['ir.sequence'].next_by_code('stock.landed.cost'),
|
||||
'Name', default=lambda self: _('New'),
|
||||
copy=False, readonly=True, track_visibility='always')
|
||||
date = fields.Date(
|
||||
'Date', default=fields.Date.context_today,
|
||||
@@ -57,6 +57,12 @@ class LandedCost(models.Model):
|
||||
def _compute_total_amount(self):
|
||||
self.amount_total = sum(line.price_unit for line in self.cost_lines)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost')
|
||||
return super(LandedCost, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
self.button_cancel()
|
||||
|
||||
@@ -227,9 +227,8 @@ class Survey(models.Model):
|
||||
|
||||
# Calculate and return statistics for choice
|
||||
if question.type in ['simple_choice', 'multiple_choice']:
|
||||
answers = {}
|
||||
comments = []
|
||||
[answers.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
|
||||
answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id}) for label in question.labels_ids)
|
||||
for input_line in question.user_input_line_ids:
|
||||
if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
|
||||
answers[input_line.value_suggested.id]['count'] += 1
|
||||
|
||||
@@ -1173,15 +1173,16 @@ define("ace/lib/regexp", ["require", "exports", "module"], function(e, t, n) {
|
||||
if (!t.onCompositionEnd || t.$readOnly) return;
|
||||
var r = h;
|
||||
h = !1;
|
||||
var i = setTimeout(function() {
|
||||
i = null;
|
||||
var i2 = setTimeout(function() {
|
||||
i2 = null;
|
||||
var e = n.value.replace(/\x01/g, "");
|
||||
if (h) return;
|
||||
e == r.lastValue ? w() : !r.lastValue && e && (w(), A(e))
|
||||
});
|
||||
k = function(n) {
|
||||
return i && clearTimeout(i), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i && t.undo(), n)
|
||||
}, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range)
|
||||
return i2 && clearTimeout(i2), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i2 && t.undo(), n)
|
||||
}, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range),
|
||||
i.isChrome && i.isChrome >= 53 && O()
|
||||
},
|
||||
I = o.delayedCall(j, 50);
|
||||
r.addListener(n, "compositionstart", B), i.isGecko ? r.addListener(n, "text", function() {
|
||||
|
||||
@@ -147,15 +147,22 @@ var Menu = Widget.extend({
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Call open_menu with the first menu_item matching an action_id
|
||||
* Call open_menu on a menu_item that matches the action_id
|
||||
*
|
||||
* If `menuID` is a match on this action, open this menu_item.
|
||||
* Otherwise open the first menu_item that matches the action_id.
|
||||
*
|
||||
* @param {Number} id the action_id to match
|
||||
* @param {Number} [menuID] a menu ID that may match with provided action
|
||||
*/
|
||||
open_action: function (id) {
|
||||
open_action: function (id, menuID) {
|
||||
var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
|
||||
var menu_id = $menu.data('menu');
|
||||
if (menu_id) {
|
||||
this.open_menu(menu_id);
|
||||
if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) {
|
||||
// menuID doesn't match action, so pick first menu_item
|
||||
menuID = $menu.data('menu');
|
||||
}
|
||||
if (menuID) {
|
||||
this.open_menu(menuID);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -135,7 +135,7 @@ return AbstractWebClient.extend({
|
||||
self.action_manager.loadState(state, !!self._current_state).then(function () {
|
||||
var action = self.action_manager.getCurrentAction();
|
||||
if (action) {
|
||||
self.menu.open_action(action.id);
|
||||
self.menu.open_action(action.id, state.menu_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -156,7 +156,7 @@ return AbstractWebClient.extend({
|
||||
var completed = $.Deferred();
|
||||
self.action_manager.doAction(result, {
|
||||
clear_breadcrumbs: true,
|
||||
action_menu_id: self.menu.current_menu,
|
||||
action_menu_id: options.id,
|
||||
}).fail(function() {
|
||||
self.menu.open_menu(options.previous_menu_id);
|
||||
}).always(function() {
|
||||
|
||||
@@ -2007,8 +2007,9 @@ var FieldToggleBoolean = AbstractField.extend({
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
var className = this.value ? 'o_toggle_button_success' : 'text-muted';
|
||||
this.$('i').addClass('fa fa-circle ' + className);
|
||||
this.$('i')
|
||||
.toggleClass('o_toggle_button_success', !!this.value)
|
||||
.toggleClass('text-muted', !this.value);
|
||||
var title = this.value ? this.attrs.options.active : this.attrs.options.inactive;
|
||||
this.$el.attr('title', title);
|
||||
},
|
||||
|
||||
@@ -878,10 +878,15 @@ var FieldX2Many = AbstractField.extend({
|
||||
this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, {
|
||||
single_page_hidden: true,
|
||||
withAccessKey: false,
|
||||
validate: function () {
|
||||
var isList = self.view.arch.tag === 'tree';
|
||||
// TODO: we should have some common method in the basic renderer...
|
||||
return isList ? self.renderer.unselectRow() : $.when();
|
||||
},
|
||||
});
|
||||
this.pager.on('pager_changed', this, function (new_state) {
|
||||
this.trigger_up('load', {
|
||||
id: this.value.id,
|
||||
self.trigger_up('load', {
|
||||
id: self.value.id,
|
||||
limit: new_state.limit,
|
||||
offset: new_state.current_min - 1,
|
||||
on_success: function (value) {
|
||||
@@ -930,6 +935,7 @@ var FieldX2Many = AbstractField.extend({
|
||||
* did not agree to discard.
|
||||
*/
|
||||
_saveLine: function (recordID) {
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
var fieldNames = this.renderer.canBeSaved(recordID);
|
||||
if (fieldNames.length) {
|
||||
@@ -941,7 +947,9 @@ var FieldX2Many = AbstractField.extend({
|
||||
} else {
|
||||
this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def));
|
||||
}
|
||||
return def;
|
||||
return def.then(function () {
|
||||
self.pager.updateState({ size: self.value.count });
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Parses the 'columnInvisibleFields' attribute to search for the domains
|
||||
@@ -1178,7 +1186,20 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') {
|
||||
if (ev.data.changes[self.name].operation === 'CREATE') {
|
||||
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
|
||||
var index = 0;
|
||||
if (self.editable !== 'top') {
|
||||
index = self.value.data.length - 1;
|
||||
// we consider that a new record, created in the bottom,
|
||||
// does not count as a record worth mentioning in the
|
||||
// pager, at least not until the line has been saved.
|
||||
// We prevent the pager from increasing its size, which
|
||||
// means that the pager is not displayed when we just
|
||||
// reach the limit. For example, if limit is 3, if we
|
||||
// have 3 records, and we click on add, we will see the
|
||||
// 4 records on the same page, but we do not want a
|
||||
// pager.
|
||||
self.pager.updateState({ size: self.value.count - 1});
|
||||
}
|
||||
var newID = self.value.data[index].id;
|
||||
self.renderer.editRecord(newID);
|
||||
}
|
||||
|
||||
@@ -359,6 +359,11 @@ var BasicModel = AbstractModel.extend({
|
||||
elem._changes = null;
|
||||
elem._isDirty = false;
|
||||
}
|
||||
elem.offset = 0;
|
||||
if (elem.tempLimitIncrement) {
|
||||
elem.limit -= elem.tempLimitIncrement;
|
||||
delete elem.tempLimitIncrement;
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -809,6 +814,7 @@ var BasicModel = AbstractModel.extend({
|
||||
this.discardChanges(id, {rollback: false});
|
||||
}
|
||||
} else if (element._changes) {
|
||||
delete element.tempLimitIncrement;
|
||||
_.each(element._changes, function (change) {
|
||||
delete change.isNew;
|
||||
});
|
||||
@@ -1640,6 +1646,12 @@ var BasicModel = AbstractModel.extend({
|
||||
// handle multiple add: command[2] may be a dict of values (1
|
||||
// record added) or an array of dict of values
|
||||
var data = _.isArray(command.ids) ? command.ids : [command.ids];
|
||||
|
||||
// Ensure the local data repository (list) boundaries can handle incoming records (data)
|
||||
if (data.length + list.res_ids.length > list.limit) {
|
||||
list.limit = data.length + list.res_ids.length;
|
||||
}
|
||||
|
||||
var list_records = {};
|
||||
_.each(data, function (d) {
|
||||
rec = self._makeDataPoint({
|
||||
@@ -1684,6 +1696,10 @@ var BasicModel = AbstractModel.extend({
|
||||
case 'CREATE':
|
||||
var options = {position: command.position};
|
||||
def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
|
||||
if (command.position === 'bottom' && list.orderedResIDs.length >= list.limit) {
|
||||
list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1;
|
||||
list.limit += 1;
|
||||
}
|
||||
// FIXME: hack for lunch widget, which does useless default_get and onchange
|
||||
if (command.data) {
|
||||
return self._applyChange(id, command.data);
|
||||
@@ -3824,7 +3840,8 @@ var BasicModel = AbstractModel.extend({
|
||||
// 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 effectiveLimit = (list.limit || 0) - (list.tempLimitIncrement || 0);
|
||||
var upperBound = effectiveLimit ? Math.min(list.offset + effectiveLimit, currentCount) : currentCount;
|
||||
var fieldNames = list.getFieldNames();
|
||||
for (var i = list.offset; i < upperBound; i++) {
|
||||
var resId = currentResIDs[i];
|
||||
|
||||
@@ -610,6 +610,10 @@ var FormRenderer = BasicRenderer.extend({
|
||||
|
||||
var $result = $('<div/>', {class: 'o_group'});
|
||||
var colSize = Math.max(1, Math.round(12 / (parseInt(node.attrs.col, 10) || 2)));
|
||||
if (node.attrs.string) {
|
||||
var $sep = $('<div/>', {class: 'o_horizontal_separator'}).text(node.attrs.string);
|
||||
$result.append($sep);
|
||||
}
|
||||
$result.append(_.map(node.children, function (child) {
|
||||
if (child.tag === 'newline') {
|
||||
return $('<br/>');
|
||||
|
||||
@@ -252,6 +252,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
}
|
||||
},
|
||||
selection_changed: function (event) {
|
||||
event.stopPropagation();
|
||||
this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length);
|
||||
},
|
||||
search: function (event) {
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
height: 20px;
|
||||
}
|
||||
.oe_kanban_bottom_left {
|
||||
.o-flex(1, 0, auto);
|
||||
.o-flex(1, 1, auto);
|
||||
|
||||
> * {
|
||||
margin-right: 6px;
|
||||
@@ -243,7 +243,7 @@
|
||||
}
|
||||
}
|
||||
.oe_kanban_bottom_right{
|
||||
.o-flex(0, 0, auto);
|
||||
.o-flex(0, 1, auto);
|
||||
|
||||
.oe_kanban_avatar {
|
||||
width: 20px;
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.o_kanban_small_column .o_kanban_group:not(.o_column_folded) {
|
||||
// [class] to get same specificity as elsewhere (kanban_view.less)
|
||||
&[class] .o_kanban_group:not(.o_column_folded) {
|
||||
.o-position-absolute(@top: @o-kanban-mobile-tabs-height, @left: 0, @bottom: 0);
|
||||
overflow-x: scroll;
|
||||
width: 100%;
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
<t t-name="WebClient.DebugManager">
|
||||
<li class="o_debug_manager">
|
||||
<a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-bug"/>
|
||||
</a>
|
||||
<ul class="dropdown-menu o_debug_dropdown" role="menu"/>
|
||||
@@ -325,12 +325,12 @@
|
||||
<t t-name="Sidebar">
|
||||
<t t-foreach="widget.sections" t-as="section">
|
||||
<div class="btn-group o_dropdown">
|
||||
<button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/>
|
||||
<t t-esc="section.label"/> <span class="caret"/>
|
||||
</button>
|
||||
<t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
|
||||
<a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank">
|
||||
<a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'" target="_blank">
|
||||
<t t-raw="item.label"/>
|
||||
</a>
|
||||
</t>
|
||||
@@ -352,7 +352,7 @@
|
||||
<t t-raw="item.write_uid[1]"/> <t t-esc="item.write_date_string"/>
|
||||
</t>
|
||||
</t>
|
||||
<a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url">
|
||||
<a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'">
|
||||
<t t-raw="item.label"/>
|
||||
<span t-if="section.name == 'files' and widget.options.editable and !item.callback" class="fa fa-trash-o o_sidebar_delete_attachment" t-att-data-id="item.id" title="Delete this attachment"/>
|
||||
</a>
|
||||
@@ -469,7 +469,7 @@
|
||||
</form>
|
||||
<t t-name="GraphView.buttons">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Measures <span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_graph_measures_list">
|
||||
@@ -498,7 +498,7 @@
|
||||
</div>
|
||||
<t t-name="PivotView.buttons">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Measures <span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_pivot_measures_list">
|
||||
@@ -852,7 +852,7 @@
|
||||
<t t-call="FieldStatus.content.button"/>
|
||||
</li>
|
||||
</ul>
|
||||
<button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown">More <span class="caret"/></button>
|
||||
<button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button>
|
||||
</t>
|
||||
<t t-foreach="selection_unfolded.reverse()" t-as="i">
|
||||
<t t-call="FieldStatus.content.button"/>
|
||||
@@ -985,7 +985,7 @@
|
||||
<ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul>
|
||||
</t>
|
||||
<div t-name="SearchView.FilterMenu" class="btn-group o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-filter"/> Filters <span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_filters_menu" role="menu">
|
||||
@@ -1097,7 +1097,7 @@
|
||||
</select>
|
||||
</t>
|
||||
<div t-name="SearchView.GroupByMenu" class="btn-group hidden-xs o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-bars"/> Group By <span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_group_by_menu" role="menu">
|
||||
@@ -1118,7 +1118,7 @@
|
||||
</li>
|
||||
</t>
|
||||
<div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-star"/> Favorites <span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_favorites_menu" role="menu">
|
||||
@@ -1247,8 +1247,8 @@
|
||||
<span class="o_stat_text"><t t-esc="text"/></span>
|
||||
</t>
|
||||
<t t-name="toggle_button">
|
||||
<button type="button" class="o_icon_button" t-att-title="widget.string">
|
||||
<i t-attf-class="fa #{widget.icon}" t-att-title="widget.string"/>
|
||||
<button type="button" class="o_icon_button" t-att-title="widget.string" t-att-aria-label="widget.string">
|
||||
<i class="fa fa-circle" t-att-title="widget.string"/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
@@ -1262,8 +1262,8 @@
|
||||
<t t-set="att_prev" t-value="{'accesskey': 'p'}" />
|
||||
<t t-set="att_next" t-value="{'accesskey': 'n'}" />
|
||||
</t>
|
||||
<button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev"/>
|
||||
<button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next"/>
|
||||
<button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous"/>
|
||||
<button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -387,7 +387,6 @@ QUnit.module('basic_fields', {
|
||||
'<field name="bar" widget="toggle_button" ' +
|
||||
'options="{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}"/>' +
|
||||
'</tree>',
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(list.$('button i.fa.fa-circle.o_toggle_button_success').length, 4,
|
||||
@@ -411,6 +410,81 @@ QUnit.module('basic_fields', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('toggle_button in form view (edit mode)', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="bar" widget="toggle_button" ' +
|
||||
'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
assert.step('write');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
res_id: 2,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
|
||||
1, "should be green");
|
||||
|
||||
// click on the button to toggle the value
|
||||
form.$('.o_field_widget[name=bar]').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
|
||||
1, "should be gray");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// save
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
|
||||
1, "should still be gray");
|
||||
assert.verifySteps(['write']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('toggle_button in form view (readonly mode)', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="bar" widget="toggle_button" ' +
|
||||
'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
assert.step('write');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
|
||||
1, "should be green");
|
||||
|
||||
// click on the button to toggle the value
|
||||
form.$('.o_field_widget[name=bar]').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
|
||||
1, "should be gray");
|
||||
assert.verifySteps(['write']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldFloat');
|
||||
|
||||
|
||||
@@ -2078,7 +2078,7 @@ QUnit.module('relational_fields', {
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bttom">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
@@ -2173,6 +2173,92 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2many list add *many* records, remove, re-add', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.fields.timmy.domain = [['color', '=', 2]];
|
||||
this.data.partner.fields.timmy.onChange = true;
|
||||
this.data.partner_type.fields.product_ids = {string: "Product", type: "many2many", relation: 'product'};
|
||||
|
||||
for (var i=0; i<50; i++) {
|
||||
var new_record_partner_type = {id: 100+i, display_name: "batch" + i, color: 2};
|
||||
this.data.partner_type.records.push(new_record_partner_type);
|
||||
}
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="product_ids" widget="many2many_tags"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'partner_type,false,list': '<tree><field name="display_name"/></tree>',
|
||||
'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'get_formview_id') {
|
||||
assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
|
||||
return $.when(false);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
// First round: add 51 records in batch
|
||||
form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
var $modal = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
$modal.find('thead input[type=checkbox]').click(); //select all the records we created in batch + 'gold'
|
||||
|
||||
$modal.find('.btn.btn-sm.btn-primary.o_select_button').click();
|
||||
|
||||
var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)');
|
||||
assert.equal(m2m_records.length, 51,
|
||||
'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold'
|
||||
|
||||
form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_save').click();
|
||||
|
||||
// Secound round: remove one record
|
||||
form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click();
|
||||
var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_delete');
|
||||
|
||||
trash_buttons.first().click();
|
||||
|
||||
var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
|
||||
assert.equal(pager_limit.text(), '50',
|
||||
'We should have 50 records in the m2m field');
|
||||
|
||||
// Third round: re-add 1 records
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
$modal = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
$modal.find('thead input[type=checkbox]').click();
|
||||
|
||||
$modal.find('.btn.btn-sm.btn-primary.o_select_button').click();
|
||||
|
||||
pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
|
||||
assert.equal(pager_limit.text(), '51',
|
||||
'We should have 51 records in the m2m field');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldOne2Many');
|
||||
|
||||
QUnit.test('one2many basic properties', function (assert) {
|
||||
@@ -2784,7 +2870,7 @@ QUnit.module('relational_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('onchange followed by edition on the second page', function (assert) {
|
||||
assert.expect(20);
|
||||
assert.expect(12);
|
||||
|
||||
var ids = [];
|
||||
for (var i=1; i<85; i++) {
|
||||
@@ -2854,10 +2940,32 @@ QUnit.module('relational_fields', {
|
||||
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange followed by edition on the second page (part 2)', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
// bottom order
|
||||
|
||||
form = createView({
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
@@ -2889,16 +2997,16 @@ QUnit.module('relational_fields', {
|
||||
|
||||
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').length, 41, "should display 41 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");
|
||||
assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "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').length, 42, "should display 42 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");
|
||||
assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3979,6 +4087,37 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, pager is updated', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
|
||||
this.data.partner.records[0].turtles = [1,2,3,4];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a record, then click in form view to confirm it
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$el.click();
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5',
|
||||
"pager should display the correct total");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('one2many list (non editable): edition', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
@@ -4287,6 +4426,185 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, adding line when only one page', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.records[0].turtles=[1,2,3];
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a record, to reach the page size limit
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
// the record currently being added should not count in the pager
|
||||
assert.ok(!form.$('.o_cp_pager').is(':visible'),
|
||||
"pager should not be visible");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, adding line, then discarding', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
|
||||
this.data.partner.records[0].turtles = [1,2,3,4];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a record, then discard
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$buttons.find('.o_form_button_cancel').click();
|
||||
|
||||
// confirm the discard operation
|
||||
$('.modal .modal-footer .btn-primary').click(); // click on confirm
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should still be visible");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, required field and pager', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
|
||||
this.data.turtle.fields.turtle_foo.required = true;
|
||||
this.data.partner.records[0].turtles = [1,2,3,4];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a (empty) record
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
// go on next page. The new record is not valid and should be discarded
|
||||
form.$('.o_pager_next').click();
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 1,
|
||||
"should have 1 data rows");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, required field, pager and confirm discard', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
|
||||
this.data.turtle.fields.turtle_foo.required = true;
|
||||
this.data.partner.records[0].turtles = [1,2,3,4];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="turtle_int"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a record with a dirty state, but not valid
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('input[name="turtle_int"]').val(4321).trigger('input');
|
||||
|
||||
// go to next page. The new record is not valid, but dirty. we should
|
||||
// see a confirm dialog
|
||||
form.$('.o_pager_next').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should still display the correct total");
|
||||
|
||||
// click on cancel
|
||||
$('.modal .modal-footer .btn-default').click(); // click on cancel
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should again display the correct total");
|
||||
assert.strictEqual(form.$('.o_field_one2many input.o_field_invalid').length, 1,
|
||||
"there should be an invalid input in the one2many");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, adding, discarding, and pager', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [1];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a 4 records record (to make the pager appear)
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
// go on next page
|
||||
form.$('.o_pager_next').click();
|
||||
|
||||
// discard
|
||||
form.$buttons.find('.o_form_button_cancel').click();
|
||||
$('.modal .modal-footer .btn-primary').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 1,
|
||||
"should have 1 data row");
|
||||
assert.ok(!form.$('.o_cp_pager').is(':visible'),
|
||||
"pager should not be visible");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('unselecting a line with missing required data', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
@@ -7602,6 +7920,42 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editing tabbed one2many (editable=bottom), again...', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.records[0].turtles = [];
|
||||
for (var i = 0; i < 9; i++) {
|
||||
var id = 100 + i;
|
||||
this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
|
||||
this.data.partner.records[0].turtles.push(id);
|
||||
}
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="3">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
|
||||
form.$('.o_x2m_control_panel .o_pager_next').click();
|
||||
form.$('.o_x2m_control_panel .o_pager_next').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 3,
|
||||
"should have 3 data rows on the current page");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editing tabbed one2many (editable=top)', function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
@@ -8217,6 +8571,49 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a new line after limit is reached should behave nicely', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [1,2,3];
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
obj.turtles = [
|
||||
[5],
|
||||
[1, 1, {turtle_foo: "yop"}],
|
||||
[1, 2, {turtle_foo: "blip"}],
|
||||
[1, 3, {turtle_foo: "kawa"}],
|
||||
[0, obj.turtles[3][2], {turtle_foo: "abc"}],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree limit="3" editable="bottom">' +
|
||||
'<field name="turtle_foo" required="1"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
assert.strictEqual(form.$('.o_data_row').length, 4, 'should have 4 data rows');
|
||||
form.$('.o_input[name="turtle_foo"]').val('a').trigger('input');
|
||||
assert.strictEqual(form.$('.o_data_row').length, 4,
|
||||
'should still have 4 data rows (the limit is increased to 4)');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange in a one2Many, but not in main record, with non inline view', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
|
||||
@@ -5202,6 +5202,46 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('outer and inner groups string attribute', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group string="parent group" class="parent_group">' +
|
||||
'<group string="child group 1" class="group_1">' +
|
||||
'<field name="bar"/>' +
|
||||
'</group>' +
|
||||
'<group string="child group 2" class="group_2">' +
|
||||
'<field name="bar"/>' +
|
||||
'</group>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
var $parentGroup = form.$('.parent_group');
|
||||
var $group1 = form.$('.group_1');
|
||||
var $group2 = form.$('.group_2');
|
||||
|
||||
assert.strictEqual(form.$('table.o_inner_group').length, 2,
|
||||
"should contain two inner groups");
|
||||
assert.strictEqual($group1.find('.o_horizontal_separator').length, 1,
|
||||
"inner group should contain one string separator");
|
||||
assert.strictEqual($group1.find('.o_horizontal_separator:contains(child group 1)').length, 1,
|
||||
"first inner group should contain 'child group 1' string");
|
||||
assert.strictEqual($group2.find('.o_horizontal_separator:contains(child group 2)').length, 1,
|
||||
"second inner group should contain 'child group 2' string");
|
||||
assert.strictEqual($parentGroup.find('> div.o_horizontal_separator:contains(parent group)').length, 1,
|
||||
"outer group should contain 'parent group' string");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('form group with newline tag inside', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ var dialogs = require('web.view_dialogs');
|
||||
var ListController = require('web.ListController');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
var FormView = require('web.FormView');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('Views', {
|
||||
beforeEach: function () {
|
||||
@@ -14,6 +17,7 @@ QUnit.module('Views', {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: {string: "Foo", type: 'char'},
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, foo: 'blip', display_name: 'blipblip', bar: true},
|
||||
@@ -21,8 +25,34 @@ QUnit.module('Views', {
|
||||
{id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true},
|
||||
],
|
||||
},
|
||||
instrument: {
|
||||
fields: {
|
||||
name: {string: "name", type: "char"},
|
||||
badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]},
|
||||
},
|
||||
},
|
||||
|
||||
badassery: {
|
||||
fields: {
|
||||
level: {string: 'level', type: "char"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, level: 'Awsome'},
|
||||
],
|
||||
},
|
||||
|
||||
product: {
|
||||
fields : {
|
||||
name: {string: "name", type: "char" },
|
||||
partner : {string: 'Doors', type: 'one2many', relation: 'partner'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: 'The end'},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
}, function () {
|
||||
|
||||
QUnit.module('view_dialogs');
|
||||
@@ -242,6 +272,98 @@ QUnit.module('Views', {
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog cascade x2many in create mode', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="partner" widget="one2many_list" >' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="instrument"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'partner,false,form': '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="instrument" widget="one2many_list" mode="tree"/>' +
|
||||
'</form>',
|
||||
|
||||
'instrument,false,form': '<form>'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="badassery">' +
|
||||
'<tree>'+
|
||||
'<field name="level"/>'+
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
|
||||
'badassery,false,list': '<tree>'+
|
||||
'<field name="level"/>'+
|
||||
'</tree>',
|
||||
|
||||
'badassery,false,search': '<search>'+
|
||||
'<field name="level"/>'+
|
||||
'</search>',
|
||||
},
|
||||
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/web/dataset/call_kw/partner/get_formview_id') {
|
||||
return $.when(false);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/instrument/get_formview_id') {
|
||||
return $.when(false);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/instrument/create') {
|
||||
assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: false}],
|
||||
'The method create should have been called with the right arguments');
|
||||
return $.when(false);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_widget .o_field_many2one[name=instrument] input').click();
|
||||
$('ul.ui-autocomplete.ui-front.ui-menu.ui-widget.ui-widget-content li.o_m2o_dropdown_option').first().click();
|
||||
|
||||
var $modal = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
$modal.find('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
var $modals = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.equal($modals.length, 2,
|
||||
'There should be two modals');
|
||||
|
||||
var $second_modal = $modals.not($modal);
|
||||
$second_modal.find('.o_list_view.table.table-condensed.table-striped.o_list_view_ungrouped .o_data_row input[type=checkbox]').click();
|
||||
|
||||
$second_modal.find('.o_select_button').click();
|
||||
|
||||
$modal = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
assert.equal($modal.find('.o_data_cell').text(), 'Awsome',
|
||||
'There should be one item in the list of the modal');
|
||||
|
||||
$modal.find('.btn.btn-sm.btn-primary').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog: save current search', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
|
||||
@@ -147,7 +147,10 @@ class Web_Editor(http.Controller):
|
||||
Attachments = request.env['ir.attachment'] # registry for the attachment table
|
||||
|
||||
res_model = kwargs.get('res_model', 'ir.ui.view')
|
||||
res_id = res_model != 'ir.ui.view' and kwargs.get('res_id') or None
|
||||
if res_model != 'ir.ui.view' and kwargs.get('res_id'):
|
||||
res_id = int(kwargs['res_id'])
|
||||
else:
|
||||
res_id = None
|
||||
|
||||
uploads = []
|
||||
message = None
|
||||
|
||||
@@ -1233,7 +1233,7 @@ registry.many2one = SnippetOption.extend({
|
||||
method: 'search_read',
|
||||
args: [domain, this.Model === 'res.partner' ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
|
||||
kwargs: {
|
||||
order: 'name DESC',
|
||||
order: [{name: 'name', asc: false}],
|
||||
limit: 5,
|
||||
context: weContext.get(),
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
word-wrap: break-word;
|
||||
ul > li > p {
|
||||
margin: 0px;
|
||||
}
|
||||
@@ -57,9 +58,9 @@
|
||||
position: static !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
overflow: visible !important;
|
||||
width: auto !important;
|
||||
overflow: hidden !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.has-error {
|
||||
.form-control {
|
||||
border-color: @state-danger-text;
|
||||
}
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
.o-appearance(none);
|
||||
background: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPScxLjEnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1 sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnIHdpZHRoPScyNCcgaGVpZ2 h0PScyNCcgdmlld0JveD0nMCAwIDI0IDI0Jz48cGF0aCBpZD0nc3ZnXzEnIGQ9J203LjQwNiw3L jgyOGw0LjU5NCw0LjU5NGw0LjU5NCwtNC41OTRsMC40MDYsMS40MDZsLTUsNC43NjZsLTUsLTQu NzY2bDAuNDA2LC0xLjQwNnonIGZpbGw9JyM4ODgnLz48L3N2Zz4=');
|
||||
|
||||
@@ -214,6 +214,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
|
||||
|
||||
self.handle_analytics_auth($analytics_components);
|
||||
gapi.analytics.auth.on('signIn', function() {
|
||||
delete window.onOriginError;
|
||||
self.handle_analytics_auth($analytics_components);
|
||||
});
|
||||
|
||||
|
||||
@@ -877,7 +877,7 @@ registry.socialShare = Animation.extend({
|
||||
var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag)
|
||||
var social_network = {
|
||||
'facebook': 'https://www.facebook.com/sharer/sharer.php?u=' + url,
|
||||
'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url),
|
||||
'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url,
|
||||
'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + url + '&title=' + encodeURIComponent(title),
|
||||
'google-plus': 'https://plus.google.com/share?url=' + url,
|
||||
};
|
||||
|
||||
@@ -63,9 +63,10 @@ options.registry.company_data = options.Class.extend({
|
||||
start: function () {
|
||||
var proto = options.registry.company_data.prototype;
|
||||
var def;
|
||||
var self = this;
|
||||
if (proto.__link === undefined) {
|
||||
def = this._rpc({route: '/web/session/get_session_info'}).then(function (session) {
|
||||
return this._rpc({
|
||||
return self._rpc({
|
||||
model: 'res.users',
|
||||
method: 'read',
|
||||
args: [session.uid, ['company_id']],
|
||||
|
||||
@@ -157,7 +157,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
|
||||
},
|
||||
};
|
||||
if (this.page.date_publish) {
|
||||
datepickersOptions.defaultDate = this.page.date_publish;
|
||||
datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish);
|
||||
}
|
||||
this.$('#date_publish_container').datetimepicker(datepickersOptions);
|
||||
|
||||
@@ -183,9 +183,15 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
|
||||
var context = weContext.get();
|
||||
var url = this.$('#page_url').val();
|
||||
|
||||
var date_publish = this.$('#date_publish').val();
|
||||
if (date_publish !== '') {
|
||||
date_publish = time.datetime_to_str(new Date(date_publish));
|
||||
var $date_publish = this.$("#date_publish");
|
||||
$date_publish.closest(".form-group").removeClass('has-error');
|
||||
var date_publish = $date_publish.val();
|
||||
if (date_publish !== "") {
|
||||
date_publish = this._parse_date(date_publish);
|
||||
if (!date_publish) {
|
||||
$date_publish.closest(".form-group").addClass('has-error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var params = {
|
||||
id: this.page.id,
|
||||
@@ -280,6 +286,28 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
|
||||
id: m[2] | 0,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Converts a string representing the browser datetime
|
||||
* (exemple: Albanian: '2018-Qer-22 15.12.35.')
|
||||
* to a string representing UTC in Odoo's datetime string format
|
||||
* (exemple: '2018-04-22 13:12:35').
|
||||
*
|
||||
* The time zone of the datetime string is assumed to be the one of the
|
||||
* browser and it will be converted to UTC (standard for Odoo).
|
||||
*
|
||||
* @private
|
||||
* @param {String} value A string representing a datetime.
|
||||
* @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise.
|
||||
*/
|
||||
_parse_date: function (value) {
|
||||
var datetime = moment(value, time.getLangDatetimeFormat(), true);
|
||||
if (datetime.isValid()) {
|
||||
return time.datetime_to_str(datetime.toDate());
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
</t>
|
||||
<t t-set="price">
|
||||
<t t-if="ticket.price or editable">
|
||||
<t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) > 1">
|
||||
<t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) > 1
|
||||
and website.get_current_pricelist().discount_policy == 'without_discount'">
|
||||
<del class="text-danger mr4" t-field="ticket.price" t-options='{
|
||||
"widget": "monetary",
|
||||
"from_currency": event.company_id.sudo().currency_id,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user