[MERGE] forward port branch 10.0 up to ffd4442b06
This commit is contained in:
@@ -486,7 +486,6 @@ class AccountInvoice(models.Model):
|
||||
addr = self.partner_id.address_get(['delivery'])
|
||||
fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery'])
|
||||
|
||||
bank_id = p.bank_ids and p.bank_ids.ids[0] or False
|
||||
|
||||
# If partner has no warning, check its company
|
||||
if p.invoice_warn == 'no-message' and p.parent_id:
|
||||
@@ -507,8 +506,12 @@ class AccountInvoice(models.Model):
|
||||
self.payment_term_id = payment_term_id
|
||||
self.fiscal_position_id = fiscal_position
|
||||
|
||||
if type in ('in_invoice', 'in_refund'):
|
||||
if type in ('in_invoice', 'out_refund'):
|
||||
bank_ids = p.commercial_partner_id.bank_ids
|
||||
bank_id = bank_ids[0].id if bank_ids else False
|
||||
self.partner_bank_id = bank_id
|
||||
return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}}
|
||||
return {}
|
||||
|
||||
|
||||
@api.onchange('journal_id')
|
||||
|
||||
@@ -709,22 +709,37 @@ class AccountMoveLine(models.Model):
|
||||
line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
|
||||
amount_currency_str = ""
|
||||
total_amount_currency_str = ""
|
||||
if line_currency != company_currency and target_currency != company_currency:
|
||||
if line_currency != company_currency and target_currency == line_currency:
|
||||
# The payment currency is the invoice currency, but they are different than the company currency
|
||||
# We use the `amount_currency` computed during the invoice validation, at the invoice date
|
||||
# to avoid exchange gain/loss
|
||||
# e.g. an invoice of 100€ must be paid with 100€, whatever the company currency and the exchange rates
|
||||
total_amount = line.amount_currency
|
||||
actual_debit = debit > 0 and amount_currency or 0.0
|
||||
actual_credit = credit > 0 and -amount_currency or 0.0
|
||||
currency = line_currency
|
||||
else:
|
||||
# Either:
|
||||
# - the invoice, payment, company currencies are all the same,
|
||||
# - the payment currency is the company currency, but the invoice currency is different,
|
||||
# - the invoice currency is the company currency, but the payment currency is different,
|
||||
# - the invoice, payment and company currencies are all different.
|
||||
# For the two first cases, we can simply use the debit/credit of the invoice move line, which are always in the company currency,
|
||||
# and this is what the target need.
|
||||
# For the two last cases, we can use the debit/credit which are in the company currency, and then change them to the target currency
|
||||
total_amount = abs(debit - credit)
|
||||
actual_debit = debit > 0 and amount or 0.0
|
||||
actual_credit = credit > 0 and -amount or 0.0
|
||||
if line_currency != target_currency and target_currency != company_currency:
|
||||
currency = company_currency
|
||||
if line_currency != target_currency:
|
||||
amount_currency_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=line_currency)
|
||||
total_amount_currency_str = formatLang(self.env, total_amount, currency_obj=line_currency)
|
||||
if currency != target_currency:
|
||||
ctx = context.copy()
|
||||
ctx.update({'date': target_date or line.date})
|
||||
total_amount = line_currency.with_context(ctx).compute(total_amount, target_currency)
|
||||
actual_debit = line_currency.with_context(ctx).compute(actual_debit, target_currency)
|
||||
actual_credit = line_currency.with_context(ctx).compute(actual_credit, target_currency)
|
||||
total_amount = currency.with_context(ctx).compute(total_amount, target_currency)
|
||||
actual_debit = currency.with_context(ctx).compute(actual_debit, target_currency)
|
||||
actual_credit = currency.with_context(ctx).compute(actual_credit, target_currency)
|
||||
amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency)
|
||||
total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class account_payment_method(models.Model):
|
||||
_name = "account.payment.method"
|
||||
_description = "Payment Methods"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True) # For internal identification
|
||||
payment_type = fields.Selection([('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True)
|
||||
|
||||
@@ -400,7 +400,7 @@ class account_payment(models.Model):
|
||||
# the writeoff debit and credit must be computed from the invoice residual in company currency
|
||||
# minus the payment amount in company currency, and not from the payment difference in the payment currency
|
||||
# to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example.
|
||||
total_residual_company_signed = self._compute_total_invoices_amount()
|
||||
total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids)
|
||||
total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id)
|
||||
if self.invoice_ids[0].type in ['in_invoice', 'out_refund']:
|
||||
amount_wo = total_payment_company_signed - total_residual_company_signed
|
||||
|
||||
@@ -1106,7 +1106,6 @@
|
||||
<field name="move_id" required="False"/>
|
||||
<newline/>
|
||||
<field name="date_maturity"/>
|
||||
<field name="date"/>
|
||||
<field name="blocked"/>
|
||||
<newline/>
|
||||
<field name="tax_ids" readonly="1"/>
|
||||
@@ -1195,7 +1194,7 @@
|
||||
<filter string="Unposted" domain="[('move_id.state','=','draft')]" help="Unposted Journal Items"/>
|
||||
<filter name="posted" string="Posted" domain="[('move_id.state','=','posted')]" help="Posted Journal Items"/>
|
||||
<separator/>
|
||||
<filter string="Unreconciled" domain="[('full_reconcile_id','=',False), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/>
|
||||
<filter string="Unreconciled" domain="[('full_reconcile_id', '=', False), ('reconciled','=', False), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/>
|
||||
<separator/>
|
||||
<filter string="Payable" domain="[('account_id.internal_type', '=', 'payable')]" help="From Payable accounts" name="payable"/>
|
||||
<filter string="Receivable" domain="[('account_id.internal_type', '=', 'receivable')]" help="From Receivable accounts" name="receivable"/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<data noupdate='1'>
|
||||
<function id="default_generate_sales_team_alias"
|
||||
model="ir.values" name="set_default"
|
||||
eval="('sale.config.settings', 'generate_sales_team_alias', True)"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -583,6 +583,9 @@
|
||||
<filter string="Overdue Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all opportunities for which the next action date is before today"/>
|
||||
<filter string="Overdue Opportunities" name="overdue_opp"
|
||||
domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all opportunities with a deadline for which the next action date is before today"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
|
||||
<filter string="Lost" domain="[('active', '=', False)]"/>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
border-bottom: 0px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.o_chat_window {
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
<field name="tax_src_id" ref="tvad_09"/>
|
||||
<field name="tax_dest_id" ref="tvatip09"/>
|
||||
</record>
|
||||
<record id="afptt_inverse_11" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_2"/>
|
||||
<field name="tax_src_id" ref="tvad_19"/>
|
||||
<field name="tax_dest_id" ref="tvatip19"/>
|
||||
</record>
|
||||
<record id="afptt_inverse_9" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_2"/>
|
||||
<field name="tax_src_id" ref="tvad_20"/>
|
||||
@@ -147,6 +152,11 @@
|
||||
<field name="tax_src_id" ref="tvad_09"/>
|
||||
<field name="tax_dest_id" ref="tvati_intrap9"/>
|
||||
</record>
|
||||
<record id="afptt_intracom_11" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_3"/>
|
||||
<field name="tax_src_id" ref="tvad_19"/>
|
||||
<field name="tax_dest_id" ref="tvati_intrap19"/>
|
||||
</record>
|
||||
<record id="afptt_intracom_13" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_3"/>
|
||||
<field name="tax_src_id" ref="tvad_20"/>
|
||||
@@ -207,6 +217,11 @@
|
||||
<field name="tax_src_id" ref="tvad_09"/>
|
||||
<field name="tax_dest_id" ref="tvati_intrap9"/>
|
||||
</record>
|
||||
<record id="afptt_intracoms_11" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_4"/>
|
||||
<field name="tax_src_id" ref="tvad_19"/>
|
||||
<field name="tax_dest_id" ref="tvati_intrap19"/>
|
||||
</record>
|
||||
<record id="afptt_intracoms_13" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_4"/>
|
||||
<field name="tax_src_id" ref="tvad_20"/>
|
||||
|
||||
@@ -135,6 +135,21 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_taxare_inversa_colectat_19" model="account.account.tag">
|
||||
<field name="name">TVA Taxare Inversa Colectat 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_taxare_inversa_deductibil_19" model="account.account.tag">
|
||||
<field name="name">TVA Taxare Inversa Deductibil 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_taxare_inversa_19" model="account.account.tag">
|
||||
<field name="name">TVA Taxare Inversa 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_taxare_inversa_colectat_20" model="account.account.tag">
|
||||
<field name="name">TVA Taxare Inversa Colectat 20%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -210,6 +225,21 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_intracomunitara_achizitii_colectat_19" model="account.account.tag">
|
||||
<field name="name">TVA Intracomunitara Achizitii Colectat 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_intracomunitara_achizitii_deductibil_19" model="account.account.tag">
|
||||
<field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_intracomunitara_achizitii_19" model="account.account.tag">
|
||||
<field name="name">TVA Intracomunitara Achizitii 19%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_tag_tva_intracomunitara_achizitii_colectat_20" model="account.account.tag">
|
||||
<field name="name">TVA Intracomunitara Achizitii Colectat 20%</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -281,7 +311,7 @@
|
||||
|
||||
<record id="tvac_19" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">11</field>
|
||||
<field name="sequence">9</field>
|
||||
<field name="name">TVA colectat 19%</field>
|
||||
<field name="description">TVA colectat 19%</field>
|
||||
<field name="amount">19</field>
|
||||
@@ -307,7 +337,7 @@
|
||||
|
||||
<record id="tvac_24" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">9</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="name">TVA colectat 24%</field>
|
||||
<field name="description">TVA colectat 24%</field>
|
||||
<field name="amount">24</field>
|
||||
@@ -359,7 +389,7 @@
|
||||
|
||||
<record id="tvad_19" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">23</field>
|
||||
<field name="sequence">19</field>
|
||||
<field name="name">TVA deductibil 19%</field>
|
||||
<field name="description">TVA deductibil 19%</field>
|
||||
<field name="amount">19</field>
|
||||
@@ -385,7 +415,7 @@
|
||||
|
||||
<record id="tvad_24" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">19</field>
|
||||
<field name="sequence">23</field>
|
||||
<field name="name">TVA deductibil 24%</field>
|
||||
<field name="description">TVA deductibil 24%</field>
|
||||
<field name="amount">24</field>
|
||||
@@ -506,6 +536,38 @@
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_9')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="tvatip19-c" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Taxare Inversa Colectat 19%</field>
|
||||
<field name="amount">-19</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="refund_account_id" ref="pcg_4427"/>
|
||||
<field name="account_id" ref="pcg_4427"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_colectat_19')])]"/>
|
||||
</record>
|
||||
<record id="tvatip19-d" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Taxare Inversa Deductibil 19%</field>
|
||||
<field name="amount">19</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="refund_account_id" ref="pcg_4426"/>
|
||||
<field name="account_id" ref="pcg_4426"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_deductibil_19')])]"/>
|
||||
</record>
|
||||
<record id="tvatip19" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">32</field>
|
||||
<field name="name">TVA Taxare Inversa 19%</field>
|
||||
<field name="description">TVA Taxare Inversa 19%</field>
|
||||
<field name="amount">1</field>
|
||||
<field name="amount_type">group</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="children_tax_ids" eval="[(6, 0, [ref('tvatip19-d'), ref('tvatip19-c')])]"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_19')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="tvatip20-c" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Taxare Inversa Colectat 20%</field>
|
||||
@@ -730,6 +792,38 @@
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_9')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="tvati_intrap19c" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Intracomunitara Achizitii Colectat 19%</field>
|
||||
<field name="amount">-19</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="refund_account_id" ref="pcg_4427"/>
|
||||
<field name="account_id" ref="pcg_4427"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_colectat_19')])]"/>
|
||||
</record>
|
||||
<record id="tvati_intrap19d" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field>
|
||||
<field name="price_include" eval="0"/>
|
||||
<field name="amount">19</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="refund_account_id" ref="pcg_4426"/>
|
||||
<field name="account_id" ref="pcg_4426"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_deductibil_19')])]"/>
|
||||
</record>
|
||||
<record id="tvati_intrap19" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="sequence">62</field>
|
||||
<field name="name">TVA Intracomunitara Achizitii 19%</field>
|
||||
<field name="amount">1</field>
|
||||
<field name="amount_type">group</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="children_tax_ids" eval="[(6, 0, [ref('tvati_intrap19d'), ref('tvati_intrap19c')])]"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_19')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="tvati_intrap20c" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||
<field name="name">TVA Intracomunitara Achizitii Colectat 20%</field>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
'data/maintenance_data.xml',
|
||||
'views/maintenance_views.xml',
|
||||
'views/maintenance_templates.xml',
|
||||
'data/maintenance_cron.xml',
|
||||
],
|
||||
'demo': ['data/maintenance_demo.xml'],
|
||||
'installable': True,
|
||||
|
||||
@@ -144,17 +144,48 @@ class MaintenanceEquipment(models.Model):
|
||||
maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team')
|
||||
maintenance_duration = fields.Float(help="Maintenance Duration in minutes and seconds.")
|
||||
|
||||
@api.depends('period', 'maintenance_open_count', 'maintenance_ids.request_date')
|
||||
@api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date')
|
||||
def _compute_next_maintenance(self):
|
||||
for equipment in self:
|
||||
create_date = equipment.create_date and datetime.strptime(equipment.create_date, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
if equipment.period:
|
||||
next_date = create_date
|
||||
if equipment.maintenance_ids:
|
||||
maintenance = equipment.maintenance_ids.sorted(lambda x: x.request_date)[0]
|
||||
next_date = maintenance.request_date and datetime.strptime(maintenance.request_date, DEFAULT_SERVER_DATE_FORMAT) or create_date
|
||||
equipment.next_action_date = next_date and (next_date + timedelta(days=equipment.period)).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
date_now = fields.Date.context_today(self)
|
||||
for equipment in self.filtered(lambda x: x.period > 0):
|
||||
next_maintenance_todo = self.env['maintenance.request'].search([
|
||||
('equipment_id', '=', equipment.id),
|
||||
('maintenance_type', '=', 'preventive'),
|
||||
('stage_id.done', '!=', True),
|
||||
('close_date', '=', False)], order="request_date asc", limit=1)
|
||||
last_maintenance_done = self.env['maintenance.request'].search([
|
||||
('equipment_id', '=', equipment.id),
|
||||
('maintenance_type', '=', 'preventive'),
|
||||
('stage_id.done', '=', True),
|
||||
('close_date', '!=', False)], order="close_date desc", limit=1)
|
||||
if next_maintenance_todo and last_maintenance_done:
|
||||
next_date = next_maintenance_todo.request_date
|
||||
date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(last_maintenance_done.close_date)
|
||||
# If the gap between the last_maintenance_done and the next_maintenance_todo one is bigger than 2 times the period and next request is in the future
|
||||
# We use 2 times the period to avoid creation too closed request from a manually one created
|
||||
if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2 and fields.Date.from_string(next_maintenance_todo.request_date) > fields.Date.from_string(date_now):
|
||||
# If the new date still in the past, we set it for today
|
||||
if fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period) < fields.Date.from_string(date_now):
|
||||
next_date = date_now
|
||||
else:
|
||||
next_date = fields.Date.to_string(fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period))
|
||||
elif next_maintenance_todo:
|
||||
next_date = next_maintenance_todo.request_date
|
||||
date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(date_now)
|
||||
# If next maintenance to do is in the future, and in more than 2 times the period, we insert an new request
|
||||
# We use 2 times the period to avoid creation too closed request from a manually one created
|
||||
if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2:
|
||||
next_date = fields.Date.to_string(fields.Date.from_string(date_now)+timedelta(days=equipment.period))
|
||||
elif last_maintenance_done:
|
||||
next_date = fields.Date.from_string(last_maintenance_done.close_date)+timedelta(days=equipment.period)
|
||||
# If when we add the period to the last maintenance done and we still in past, we plan it for today
|
||||
if next_date < fields.Date.from_string(date_now):
|
||||
next_date = date_now
|
||||
else:
|
||||
next_date = fields.Date.to_string(fields.Date.from_string(date_now) + timedelta(days=equipment.period))
|
||||
|
||||
equipment.next_action_date = next_date
|
||||
@api.one
|
||||
@api.depends('maintenance_ids.stage_id.done')
|
||||
def _compute_maintenance_count(self):
|
||||
@@ -190,18 +221,31 @@ class MaintenanceEquipment(models.Model):
|
||||
category_ids = categories._search([], order=order, access_rights_uid=SUPERUSER_ID)
|
||||
return categories.browse(category_ids)
|
||||
|
||||
def _create_new_request(self, date):
|
||||
self.ensure_one()
|
||||
self.env['maintenance.request'].create({
|
||||
'name': _('Preventive Maintenance - %s') % self.name,
|
||||
'request_date': date,
|
||||
'schedule_date': date,
|
||||
'category_id': self.category_id.id,
|
||||
'equipment_id': self.id,
|
||||
'maintenance_type': 'preventive',
|
||||
'owner_user_id': self.owner_user_id.id,
|
||||
'technician_user_id': self.technician_user_id.id,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _cron_generate_requests(self):
|
||||
for equipment in self.search([]):
|
||||
if equipment.period and equipment.next_action_date == date.today().strftime(DEFAULT_SERVER_DATE_FORMAT):
|
||||
self.env['maintenance.request'].create({
|
||||
'name': _('Preventive Maintenance - %s' % equipment.next_action_date),
|
||||
'request_date': equipment.next_action_date,
|
||||
'category_id': equipment.category_id.id,
|
||||
'equipment_id': equipment.id,
|
||||
'maintenance_type': 'preventive',
|
||||
})
|
||||
|
||||
"""
|
||||
Generates maintenance request on the next_action_date or today if none exists
|
||||
"""
|
||||
for equipment in self.search([('period', '>', 0)]):
|
||||
next_requests = self.env['maintenance.request'].search([('stage_id.done', '=', False),
|
||||
('equipment_id', '=', equipment.id),
|
||||
('maintenance_type', '=', 'preventive'),
|
||||
('request_date', '=', equipment.next_action_date)])
|
||||
if not next_requests:
|
||||
equipment._create_new_request(equipment.next_action_date)
|
||||
|
||||
class MaintenanceRequest(models.Model):
|
||||
_name = 'maintenance.request'
|
||||
@@ -227,8 +271,8 @@ class MaintenanceRequest(models.Model):
|
||||
|
||||
name = fields.Char('Subjects', required=True)
|
||||
description = fields.Text('Description')
|
||||
request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today)
|
||||
|
||||
request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today,
|
||||
help="Date requested for the maintenance to happen")
|
||||
owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid)
|
||||
category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True)
|
||||
equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True)
|
||||
@@ -237,13 +281,13 @@ class MaintenanceRequest(models.Model):
|
||||
group_expand='_read_group_stage_ids', default=_default_stage)
|
||||
priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority')
|
||||
color = fields.Integer('Color Index')
|
||||
close_date = fields.Date('Close Date')
|
||||
close_date = fields.Date('Close Date', help="Date the maintenance was finished. ")
|
||||
kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')],
|
||||
string='Kanban State', required=True, default='normal', track_visibility='onchange')
|
||||
# active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.")
|
||||
archive = fields.Boolean(default=False, help="Set archive to true to hide the maintenance request without deleting it.")
|
||||
maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective")
|
||||
schedule_date = fields.Datetime('Scheduled Date')
|
||||
schedule_date = fields.Datetime('Scheduled Date', help="Date the maintenance team plans the maintenance. It should not differ much from the Request Date. ")
|
||||
maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id)
|
||||
duration = fields.Float(help="Duration in minutes and seconds.")
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import time
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from dateutil import relativedelta
|
||||
import datetime
|
||||
|
||||
class TestEquipment(TransactionCase):
|
||||
""" Test used to check that when doing equipment/maintenance_request/equipment_category creation."""
|
||||
@@ -36,7 +37,7 @@ class TestEquipment(TransactionCase):
|
||||
groups_id=[(6, 0, [res_manager.id])]
|
||||
))
|
||||
|
||||
def test_equipment_request_category(self):
|
||||
def test_10_equipment_request_category(self):
|
||||
|
||||
# Create a new equipment
|
||||
equipment_01 = self.equipment.sudo(self.manager).create({
|
||||
@@ -75,3 +76,32 @@ class TestEquipment(TransactionCase):
|
||||
|
||||
# I check that maintenance request is in the "In Progress" stage
|
||||
self.assertEquals(maintenance_request_01.stage_id.id, self.ref('maintenance.stage_1'))
|
||||
|
||||
def test_20_cron(self):
|
||||
""" Check the cron creates the necessary preventive maintenance requests"""
|
||||
equipment_cron = self.equipment.create({
|
||||
'name': 'High Maintenance Monitor because of Color Calibration',
|
||||
'category_id': self.ref('maintenance.equipment_monitor'),
|
||||
'technician_user_id': self.ref('base.user_root'),
|
||||
'owner_user_id': self.user.id,
|
||||
'assign_date': time.strftime('%Y-%m-%d'),
|
||||
'period': 7,
|
||||
'color': 3,
|
||||
})
|
||||
|
||||
maintenance_request_cron = self.maintenance_request.create({
|
||||
'name': 'Need a special calibration',
|
||||
'technician_user_id': self.user.id,
|
||||
'request_date': (datetime.datetime.now() + relativedelta.relativedelta(days=7)).strftime('%Y-%m-%d'),
|
||||
'maintenance_type': 'preventive',
|
||||
'owner_user_id': self.user.id,
|
||||
'equipment_id': equipment_cron.id,
|
||||
'color': 7,
|
||||
'stage_id': self.ref('maintenance.stage_0'),
|
||||
'maintenance_team_id': self.ref('maintenance.equipment_team_maintenance')
|
||||
})
|
||||
|
||||
self.env['maintenance.equipment']._cron_generate_requests()
|
||||
# As it is generating the requests for one month in advance, we should have 4 requests in total
|
||||
tot_requests = self.maintenance_request.search([('equipment_id', '=', equipment_cron.id)])
|
||||
self.assertEqual(len(tot_requests), 1, 'The cron should have generated just 1 request for the High Maintenance Monitor.')
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<field name="owner_user_id" string="Requested By"/>
|
||||
<field name="equipment_id" context="{'default_category_id':category_id}"/>
|
||||
<field name="category_id" groups="maintenance.group_equipment_manager"/>
|
||||
<field name="request_date" groups="base.group_no_one"/>
|
||||
<field name="request_date"/>
|
||||
<field name="close_date" groups="base.group_no_one"/>
|
||||
<field name="archive" invisible="1"/>
|
||||
<field name="maintenance_type" widget="radio"/>
|
||||
@@ -416,7 +416,7 @@
|
||||
<div class="oe_kanban_footer_left mt4 label label-danger" t-if="record.maintenance_open_count.raw_value" >
|
||||
<t t-raw="record.maintenance_open_count.raw_value"/> Request
|
||||
</div>
|
||||
<div class="oe_kanban_footer_left mt4 label label-default" t-if="record.maintenance_open_count.raw_value == 0 and record.next_action_date.raw_value" >
|
||||
<div class="oe_kanban_footer_left mt4 label label-default" t-if="record.next_action_date.raw_value" >
|
||||
<t t-raw="moment(record.next_action_date.raw_value).format('MMMM Do')"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,6 +323,7 @@ class MrpProduction(models.Model):
|
||||
data = {
|
||||
'name': self.name,
|
||||
'date': self.date_planned_start,
|
||||
'date_expected': self.date_planned_start,
|
||||
'bom_line_id': bom_line.id,
|
||||
'product_id': bom_line.product_id.id,
|
||||
'product_uom_qty': quantity,
|
||||
|
||||
@@ -81,14 +81,14 @@ class StockMove(models.Model):
|
||||
store=True,
|
||||
help='Technical Field to order moves') # TDE: what ?
|
||||
|
||||
@api.multi
|
||||
@api.depends('state', 'product_uom_qty', 'reserved_availability')
|
||||
def _qty_available(self):
|
||||
for move in self:
|
||||
# For consumables, state is available so availability = qty to do
|
||||
if move.state == 'assigned':
|
||||
move.quantity_available = move.product_uom_qty
|
||||
else:
|
||||
move.quantity_available = move.reserved_availability
|
||||
elif move.product_id.uom_id and move.product_uom:
|
||||
move.quantity_available = move.product_id.uom_id._compute_quantity(move.reserved_availability, move.product_uom)
|
||||
|
||||
@api.multi
|
||||
@api.depends('move_lot_ids', 'move_lot_ids.quantity_done', 'quantity_done_store')
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<field name="product_uom_id" readonly="1"/>
|
||||
</div>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/>
|
||||
<field name="lot_id" required="1" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -198,6 +198,7 @@ class PosOrder(models.Model):
|
||||
|
||||
grouped_data = {}
|
||||
have_to_group_by = session and session.config_id.group_by or False
|
||||
rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method
|
||||
|
||||
for order in self.filtered(lambda o: not o.account_move or order.state == 'paid'):
|
||||
current_company = order.sale_journal.company_id
|
||||
@@ -296,6 +297,14 @@ class PosOrder(models.Model):
|
||||
'partner_id': partner_id
|
||||
})
|
||||
|
||||
# round tax lines per order
|
||||
if rounding_method == 'round_globally':
|
||||
for group_key, group_value in grouped_data.iteritems():
|
||||
if group_key[0] == 'tax':
|
||||
for line in group_value:
|
||||
line['credit'] = cur.round(line['credit'])
|
||||
line['debit'] = cur.round(line['debit'])
|
||||
|
||||
# counterpart
|
||||
insert_data('counter_part', {
|
||||
'name': _("Trade Receivables"), # order.name,
|
||||
|
||||
@@ -278,13 +278,19 @@ var DebugWidget = PosBaseWidget.extend({
|
||||
});
|
||||
|
||||
this.$('.button.export_unpaid_orders').click(function(){
|
||||
self.gui.download_file(self.pos.export_unpaid_orders(),
|
||||
"unpaid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json');
|
||||
self.gui.prepare_download_link(
|
||||
self.pos.export_unpaid_orders(),
|
||||
_t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
|
||||
".export_unpaid_orders", ".download_unpaid_orders"
|
||||
);
|
||||
});
|
||||
|
||||
this.$('.button.export_paid_orders').click(function() {
|
||||
self.gui.download_file(self.pos.export_paid_orders(),
|
||||
"paid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json');
|
||||
self.gui.prepare_download_link(
|
||||
self.pos.export_paid_orders(),
|
||||
_t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
|
||||
".export_paid_orders", ".download_paid_orders"
|
||||
);
|
||||
});
|
||||
|
||||
this.$('.button.import_orders input').on('change', function(event) {
|
||||
|
||||
@@ -362,7 +362,34 @@ var Gui = core.Class.extend({
|
||||
// if 'contents' is not a string, it is converted into
|
||||
// a JSON representation of the contents.
|
||||
|
||||
|
||||
// TODO: remove me in master: deprecated in favor of prepare_download_link
|
||||
// this method is kept for backward compatibility but is likely not going
|
||||
// to work as many browsers do to not accept fake click events on links
|
||||
download_file: function(contents, name) {
|
||||
href_params = this.prepare_file_blob(contents,name);
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent("click");
|
||||
|
||||
$("<a>",href_params).get(0).dispatchEvent(evt);
|
||||
|
||||
},
|
||||
|
||||
prepare_download_link: function(contents, filename, src, target) {
|
||||
var href_params = this.prepare_file_blob(contents, filename);
|
||||
|
||||
$(target).parent().attr(href_params);
|
||||
$(src).addClass('oe_hidden');
|
||||
$(target).removeClass('oe_hidden');
|
||||
|
||||
// hide again after click
|
||||
$(target).click(function() {
|
||||
$(src).removeClass('oe_hidden');
|
||||
$(this).addClass('oe_hidden');
|
||||
});
|
||||
},
|
||||
|
||||
prepare_file_blob: function(contents, name) {
|
||||
var URL = window.URL || window.webkitURL;
|
||||
|
||||
if (typeof contents !== 'string') {
|
||||
@@ -371,13 +398,8 @@ var Gui = core.Class.extend({
|
||||
|
||||
var blob = new Blob([contents]);
|
||||
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent("click");
|
||||
|
||||
$("<a>",{
|
||||
download: name || 'document.txt',
|
||||
href: URL.createObjectURL(blob),
|
||||
}).get(0).dispatchEvent(evt);
|
||||
return {download: name || 'document.txt',
|
||||
href: URL.createObjectURL(blob),}
|
||||
},
|
||||
|
||||
/* ---- Gui: EMAILS ---- */
|
||||
|
||||
@@ -105,7 +105,9 @@ var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({
|
||||
this._super(opts);
|
||||
|
||||
this.$('.download').off('click').click(function(){
|
||||
self.gui.download_file(self.options.body,'traceback.txt');
|
||||
self.gui.prepare_download_link(self.options.body,
|
||||
_t('error') + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.txt',
|
||||
'.download', '.download_error_file');
|
||||
});
|
||||
|
||||
this.$('.email').off('click').click(function(){
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
</div>
|
||||
<div class='client-detail'>
|
||||
<span class='label'>Country</span>
|
||||
<select class='detail client-address-country' name='country_id'>
|
||||
<select class='detail client-address-country needsclick' name='country_id'>
|
||||
<option value=''>None</option>
|
||||
<t t-foreach='widget.pos.countries' t-as='country'>
|
||||
<option t-att-value='country.id' t-att-selected="partner.country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined">
|
||||
@@ -975,6 +975,9 @@
|
||||
<div class="button cancel">
|
||||
Ok
|
||||
</div>
|
||||
<a><div class="button icon download_error_file oe_hidden">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</div></a>
|
||||
<div class="button icon download">
|
||||
<i class="fa fa-download"></i>
|
||||
</div>
|
||||
@@ -1308,7 +1311,9 @@
|
||||
<li class="button delete_orders">Delete Paid Orders</li>
|
||||
<li class="button delete_unpaid_orders">Delete Unpaid Orders</li>
|
||||
<li class="button export_paid_orders">Export Paid Orders</li>
|
||||
<a><li class="button download_paid_orders oe_hidden">Download Paid Orders</li></a>
|
||||
<li class="button export_unpaid_orders">Export Unpaid Orders</li>
|
||||
<a><li class="button download_unpaid_orders oe_hidden">Download Unpaid Orders</li></a>
|
||||
<li class="button import_orders" style="position:relative">
|
||||
Import Orders
|
||||
<input type="file" style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer"/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<field name="name">report.pos.order.pivot</field>
|
||||
<field name="model">report.pos.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Point of Sale Analysis">
|
||||
<pivot string="Point of Sale Analysis" disable_linking="True">
|
||||
<field name="product_categ_id" type="row"/>
|
||||
<field name="date" interval="month" type="col"/>
|
||||
<field name="product_qty" type="measure"/>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<group>
|
||||
<group name="group_general">
|
||||
<field name="type"/>
|
||||
<field name="categ_id" string="Internal Category"/>
|
||||
</group>
|
||||
<group name="group_standard_price">
|
||||
<field name="list_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
@@ -310,6 +311,7 @@
|
||||
<field name="item_ids" position="replace">
|
||||
<field name="pricelist_item_ids" widget="many2many">
|
||||
<tree string="Pricelist Items">
|
||||
<field name="pricelist_id"/>
|
||||
<field name="name" string="Applicable On"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
|
||||
@@ -448,6 +448,7 @@ class PurchaseOrder(models.Model):
|
||||
result = action.read()[0]
|
||||
|
||||
#override the context to get rid of the default filtering on picking type
|
||||
result.pop('id', None)
|
||||
result['context'] = {}
|
||||
pick_ids = sum([order.picking_ids.ids for order in self], [])
|
||||
#choose the view_mode accordingly
|
||||
|
||||
@@ -17,7 +17,7 @@ class ir_actions_report(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
action_ref = self.env.ref('base.action_ui_view')
|
||||
if not action_ref:
|
||||
if not action_ref or len(self.report_name.split('.')) < 2:
|
||||
return False
|
||||
action_data = action_ref.read()[0]
|
||||
action_data['domain'] = [('name', 'ilike', self.report_name.split('.')[1]), ('type', '=', 'qweb')]
|
||||
|
||||
@@ -29,9 +29,11 @@
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.view_template_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="categ_id" position="after">
|
||||
<field name="intrastat_id"/>
|
||||
</field>
|
||||
<group name="group_lots_and_weight" position="after">
|
||||
<group>
|
||||
<field name="intrastat_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -506,10 +506,12 @@ class SaleOrder(models.Model):
|
||||
for tax in line.tax_id:
|
||||
group = tax.tax_group_id
|
||||
res.setdefault(group, 0.0)
|
||||
amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty)['taxes'][0]['amount']
|
||||
amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty,
|
||||
product=line.product_id, partner=line.order_partner_id)['taxes'][0]['amount']
|
||||
res[group] += amount
|
||||
if tax.include_base_amount:
|
||||
base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1)['taxes'][0]['amount']
|
||||
base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1, product=line.product_id,
|
||||
partner=line.order_partner_id)['taxes'][0]['amount']
|
||||
res = sorted(res.items(), key=lambda l: l[0].sequence)
|
||||
res = map(lambda l: (l[0].name, l[1]), res)
|
||||
return res
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="show_demo or (values and values['closing']['overdue'] != 0)" class="o_warning" title="To Opportunities">
|
||||
<a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_activities_overdue':'1'}">
|
||||
<a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_overdue_opp':'1'}">
|
||||
<t t-if="!show_demo">
|
||||
<t t-esc="values['closing']['overdue']"/> exp. closing
|
||||
</t>
|
||||
|
||||
@@ -34,8 +34,7 @@ class PackOperation(models.Model):
|
||||
product_qty = fields.Float('To Do', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True)
|
||||
ordered_qty = fields.Float('Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'))
|
||||
qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'))
|
||||
# TDE FIXME: what what what what ??
|
||||
is_done = fields.Boolean(compute='_compute_is_done', inverse='_set_is_done', string='Done', oldname='processed_boolean')
|
||||
is_done = fields.Boolean(compute='_compute_is_done', string='Done', readonly=False, oldname='processed_boolean')
|
||||
package_id = fields.Many2one('stock.quant.package', 'Source Package')
|
||||
pack_lot_ids = fields.One2many('stock.pack.operation.lot', 'operation_id', 'Lots/Serial Numbers Used')
|
||||
result_package_id = fields.Many2one(
|
||||
@@ -73,9 +72,8 @@ class PackOperation(models.Model):
|
||||
def _compute_is_done(self):
|
||||
self.is_done = self.qty_done > 0.0
|
||||
|
||||
@api.one
|
||||
def _set_is_done(self):
|
||||
# TDE FIXME: whuuuut ???
|
||||
@api.onchange('is_done')
|
||||
def on_change_is_done(self):
|
||||
if not self.product_id:
|
||||
if self.is_done and self.qty_done == 0:
|
||||
self.qty_done = 1.0
|
||||
|
||||
@@ -1366,7 +1366,8 @@ class TestStockFlow(TestStockCommon):
|
||||
|
||||
# set the pack in pack operation to 'done'
|
||||
for pack in picking_pack.pack_operation_pack_ids:
|
||||
pack.qty_done = 1.0
|
||||
pack.is_done = True
|
||||
pack.on_change_is_done()
|
||||
|
||||
# Put in a pack
|
||||
picking_pack.put_in_pack()
|
||||
@@ -1384,7 +1385,8 @@ class TestStockFlow(TestStockCommon):
|
||||
|
||||
# set the pack in pack operation to 'done'
|
||||
for pack in picking_out.pack_operation_pack_ids:
|
||||
pack.qty_done = 1.0
|
||||
pack.is_done = True
|
||||
pack.on_change_is_done()
|
||||
|
||||
# Validate picking
|
||||
picking_out.do_new_transfer()
|
||||
|
||||
@@ -140,9 +140,6 @@
|
||||
<field name="volume" string="Volume"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="categ_id" string="Internal Category"/>
|
||||
</group>
|
||||
<group name="stock_property" groups="base.group_no_one">
|
||||
<field name="property_stock_procurement" domain="[('usage','=','procurement')]"/>
|
||||
<field name="property_stock_production" domain="[('usage','=','production')]"/>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(lot_open_quants)d" icon="fa-arrows" class="oe_stat_button" string="Locate" type="action" attrs="{'invisible': [('quant_ids','=',[])]}"/>
|
||||
<button name="action_traceability" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="object" attrs="{'invisible': [('quant_ids','=',[])]}"/>
|
||||
<field name="quant_ids" invisible="True"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
|
||||
@@ -79,7 +79,7 @@ class StockHistory(models.Model):
|
||||
product_template_id,
|
||||
SUM(quantity) as quantity,
|
||||
date,
|
||||
SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
|
||||
COALESCE(SUM(price_unit_on_quant * quantity) / NULLIF(SUM(quantity), 0), 0) as price_unit_on_quant,
|
||||
source,
|
||||
string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number
|
||||
FROM
|
||||
|
||||
@@ -638,7 +638,7 @@ class SurveyQuestion(models.Model):
|
||||
if self.comments_allowed:
|
||||
comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
|
||||
# Preventing answers with blank value
|
||||
if all([True if not answer.strip() else False for answer in answer_candidates.values()]):
|
||||
if all([True if not answer.strip() else False for answer in answer_candidates.values()]) and answer_candidates:
|
||||
errors.update({answer_tag: self.constr_error_msg})
|
||||
# There is no answer neither comments (if comments count as answer)
|
||||
if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
|
||||
|
||||
@@ -86,8 +86,11 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
* Init a session, reloads from cookie, if it exists
|
||||
*/
|
||||
session_init: function () {
|
||||
var def = this.session_reload();
|
||||
if (this.is_frontend) return def;
|
||||
|
||||
var self = this;
|
||||
return this.session_reload().then(function() {
|
||||
return def.then(function() {
|
||||
var modules = self.module_list.join(',');
|
||||
var deferred = self.load_qweb(modules);
|
||||
if(self.session_is_valid()) {
|
||||
|
||||
@@ -26,7 +26,7 @@ var M2ODialog = Dialog.extend({
|
||||
title: _.str.sprintf(_t("Create a %s"), parent.string),
|
||||
size: 'medium',
|
||||
buttons: [
|
||||
{text: _t('Create'), classes: 'btn-primary', click: function() {
|
||||
{text: _t('Create'), classes: 'btn-primary', click: function(e) {
|
||||
if (this.$("input").val() !== ''){
|
||||
this.getParent()._quick_create(this.$("input").val());
|
||||
this.close();
|
||||
@@ -997,7 +997,9 @@ var X2ManyListView = ListView.extend({
|
||||
*/
|
||||
var X2ManyList = ListView.List.extend({
|
||||
pad_table_to: function (count) {
|
||||
if (!this.view.is_action_enabled('create') || this.view.x2m.get('effective_readonly')) {
|
||||
var ftype = this.view.x2m.field.type;
|
||||
var is_readonly = this.view.x2m.get('effective_readonly');
|
||||
if (is_readonly || (ftype === 'one2many' && !this.view.is_action_enabled('create'))) {
|
||||
this._super(count);
|
||||
return;
|
||||
}
|
||||
@@ -1304,7 +1306,7 @@ var Many2ManyListView = X2ManyListView.extend({
|
||||
context: this.x2m.build_context(),
|
||||
title: _t("Add: ") + this.x2m.string,
|
||||
alternative_form_view: this.x2m.field.views ? this.x2m.field.views.form : undefined,
|
||||
no_create: this.x2m.options.no_create,
|
||||
no_create: this.x2m.options.no_create || !this.is_action_enabled('create'),
|
||||
on_selected: function(element_ids) {
|
||||
return self.x2m.data_link_multi(element_ids).then(function() {
|
||||
self.x2m.reload_current_view();
|
||||
|
||||
@@ -1508,7 +1508,7 @@ ListView.Groups = Class.extend({
|
||||
// drag and drop enabled if list is not sorted (unless it is sorted by
|
||||
// its sequence field (ASC)), and there is a visible column with
|
||||
// @widget=handle or "sequence" column in the view.
|
||||
if ((dataset.sort && [seqname, seqname + 'ASC', ''].indexOf(dataset.sort()) === -1)
|
||||
if ((dataset.sort && [seqname, seqname + ' ASC', ''].indexOf(dataset.sort()) === -1)
|
||||
|| !_(this.columns).findWhere({'name': seqname})) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ var PivotView = View.extend({
|
||||
this.headers = {};
|
||||
this.cells = {};
|
||||
this.has_data = false;
|
||||
this.$buttons = $();
|
||||
|
||||
this.last_header_selected = null;
|
||||
this.sorted_column = {};
|
||||
|
||||
@@ -34,20 +34,16 @@ class WebsiteSaleDigital(website_account):
|
||||
return response
|
||||
order = response.qcontext['order']
|
||||
invoiced_lines = request.env['account.invoice.line'].sudo().search([('invoice_id', 'in', order.invoice_ids.ids), ('invoice_id.state', '=', 'paid')])
|
||||
products = invoiced_lines.mapped('product_id') | order.order_line.filtered(lambda r: not r.price_subtotal).mapped('product_id')
|
||||
|
||||
purchased_products_attachments = {}
|
||||
for il in invoiced_lines:
|
||||
product = il.product_id
|
||||
# Ignore products that do not have digital content
|
||||
if not product.product_tmpl_id.type == 'digital':
|
||||
continue
|
||||
|
||||
for product in products:
|
||||
# Search for product attachments
|
||||
Attachment = request.env['ir.attachment']
|
||||
product_id = product.id
|
||||
template = product.product_tmpl_id
|
||||
att = Attachment.search_read(
|
||||
domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id)],
|
||||
domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)],
|
||||
fields=['name', 'write_date'],
|
||||
order='write_date desc',
|
||||
)
|
||||
|
||||
@@ -13,7 +13,13 @@ class AccountInvoiceLine(models.Model):
|
||||
|
||||
# Get paid invoices
|
||||
purchases = self.sudo().search_read(
|
||||
domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id), ('product_id.product_tmpl_id.type', '=', 'digital')],
|
||||
domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id)],
|
||||
fields=['product_id'],
|
||||
)
|
||||
|
||||
# Get free products
|
||||
purchases += self.env['sale.order.line'].sudo().search_read(
|
||||
domain=[('price_subtotal', '=', 0.0), ('order_id.partner_id', '=', partner.id)],
|
||||
fields=['product_id'],
|
||||
)
|
||||
|
||||
|
||||
@@ -494,11 +494,12 @@ class Slide(models.Model):
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _fetch_data(self, base_url, data, content_type=False):
|
||||
def _fetch_data(self, base_url, data, content_type=False, extra_params=False):
|
||||
result = {'values': dict()}
|
||||
try:
|
||||
if data:
|
||||
base_url = base_url + '?%s' % urlencode(data)
|
||||
sep = '?' if not extra_params else '&'
|
||||
base_url = base_url + '%s%s' % (sep, urlencode(data))
|
||||
req = urllib2.Request(base_url)
|
||||
content = urllib2.urlopen(req).read()
|
||||
if content_type == 'json':
|
||||
@@ -566,12 +567,13 @@ class Slide(models.Model):
|
||||
def _parse_google_document(self, document_id, only_preview_fields):
|
||||
def get_slide_type(vals):
|
||||
# TDE FIXME: WTF ??
|
||||
image = Image.open(io.BytesIO(vals['image'].decode('base64')))
|
||||
width, height = image.size
|
||||
if height > width:
|
||||
return 'document'
|
||||
else:
|
||||
return 'presentation'
|
||||
slide_type = 'presentation'
|
||||
if vals.get('image'):
|
||||
image = Image.open(io.BytesIO(vals['image'].decode('base64')))
|
||||
width, height = image.size
|
||||
if height > width:
|
||||
return 'document'
|
||||
return slide_type
|
||||
|
||||
# Google drive doesn't use a simple API key to access the data, but requires an access
|
||||
# token. However, this token is generated in module google_drive, which is not in the
|
||||
@@ -609,12 +611,14 @@ class Slide(models.Model):
|
||||
values['datas'] = values['image']
|
||||
values['slide_type'] = 'infographic'
|
||||
elif google_values['mimeType'].startswith('application/vnd.google-apps'):
|
||||
values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], {}, 'pdf')['values']
|
||||
values['slide_type'] = get_slide_type(values)
|
||||
if google_values['exportLinks'].get('text/plain'):
|
||||
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], {})['values']
|
||||
if google_values['exportLinks'].get('text/csv'):
|
||||
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], {})['values']
|
||||
if 'exportLinks' in google_values:
|
||||
values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], params, 'pdf', extra_params=True)['values']
|
||||
# Content indexing
|
||||
if google_values['exportLinks'].get('text/plain'):
|
||||
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], params, extra_params=True)['values']
|
||||
elif google_values['exportLinks'].get('text/csv'):
|
||||
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], params, extra_params=True)['values']
|
||||
elif google_values['mimeType'] == 'application/pdf':
|
||||
# TODO: Google Drive PDF document doesn't provide plain text transcript
|
||||
values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values']
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Belgium, 30/01/2017
|
||||
|
||||
Somko agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Frederik Santens info@somko.be https://github.com/somko
|
||||
|
||||
List of contributors:
|
||||
|
||||
Frederik Santens info@somko.be https://github.com/somko
|
||||
Thomas Vanmellaerts thomas.vanmellaerts@somko.be https://github.com/thomassomko
|
||||
Kasper Declercq kasper.declercq@somko.be https://github.com/kasperdeclercq
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
China, 2017-02-06
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Yue Qin 602559817@qq.com https://github.com/yueqin
|
||||
@@ -207,9 +207,9 @@ RPM
|
||||
|
||||
.. warning::
|
||||
|
||||
with RHEL-based distributions (RHEL, CenOS, Scientific Linux), EPEL_ must
|
||||
with RHEL-based distributions (RHEL, CentOS, Scientific Linux), EPEL_ must
|
||||
be added to the distribution's repositories for all of Odoo's
|
||||
dependencies to be available. For CenOS:
|
||||
dependencies to be available. For CentOS:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
|
||||
@@ -611,10 +611,11 @@ class IrActionsServer(models.Model):
|
||||
:type action: browse record
|
||||
:returns: dict -- evaluation context given to (safe_)safe_eval """
|
||||
def log(message, level="info"):
|
||||
self._cr.execute("""
|
||||
INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
|
||||
VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name))
|
||||
with self.pool.cursor() as cr:
|
||||
cr.execute("""
|
||||
INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
|
||||
VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name))
|
||||
|
||||
eval_context = super(IrActionsServer, self)._get_eval_context(action=action)
|
||||
model = self.env[action.model_id.model]
|
||||
|
||||
@@ -1340,7 +1340,7 @@ class IrModelData(models.Model):
|
||||
class WizardModelMenu(models.TransientModel):
|
||||
_name = 'wizard.ir.model.menu.create'
|
||||
|
||||
menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True)
|
||||
menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True, ondelete='cascade')
|
||||
name = fields.Char(string='Menu Name', required=True)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -55,7 +55,7 @@ class IrUiMenu(models.Model):
|
||||
if level <= 0:
|
||||
return '...'
|
||||
if self.parent_id:
|
||||
return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name
|
||||
return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "")
|
||||
else:
|
||||
return self.name
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ class Partner(models.Model):
|
||||
sync_children = self.child_ids.filtered(lambda c: not c.is_company)
|
||||
for child in sync_children:
|
||||
child._commercial_sync_to_children()
|
||||
sync_children._compute_commercial_partner()
|
||||
return sync_children.write(sync_vals)
|
||||
|
||||
@api.multi
|
||||
@@ -453,6 +454,10 @@ class Partner(models.Model):
|
||||
commercial_fields = self._commercial_fields()
|
||||
if any(field in values for field in commercial_fields):
|
||||
self._commercial_sync_to_children()
|
||||
for child in self.child_ids.filtered(lambda c: not c.is_company):
|
||||
if child.commercial_partner_id != self.commercial_partner_id :
|
||||
self._commercial_sync_to_children()
|
||||
break
|
||||
# 2b. Address fields: sync if address changed
|
||||
address_fields = self._address_fields()
|
||||
if any(field in values for field in address_fields):
|
||||
|
||||
+1
-1
@@ -2001,7 +2001,7 @@ class BaseModel(object):
|
||||
prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else ''
|
||||
|
||||
query = """
|
||||
SELECT min(%(table)s.id) AS id, count(%(table)s.id) AS %(count_field)s %(extra_fields)s
|
||||
SELECT min("%(table)s".id) AS id, count("%(table)s".id) AS "%(count_field)s" %(extra_fields)s
|
||||
FROM %(from)s
|
||||
%(where)s
|
||||
%(groupby)s
|
||||
|
||||
Reference in New Issue
Block a user