mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-25 15:30:05 -05:00
[PATCH] Upstream patch - 01092021
This commit is contained in:
1 parent
ee040cbd30
commit
167ec0116a
55 files changed
+621
-169
No files matched your search
@@ -325,7 +325,8 @@ class AccountJournal(models.Model):
|
||||
|
||||
@api.constrains('active')
|
||||
def _check_auto_post_draft_entries(self):
|
||||
for journal in self:
|
||||
# constraint should be tested just after archiving a journal, but shouldn't be raised when unarchiving a journal containing draft entries
|
||||
for journal in self.filtered(lambda j: not j.active):
|
||||
pending_moves = self.env['account.move'].search([
|
||||
('journal_id', '=', journal.id),
|
||||
('state', '=', 'draft')
|
||||
|
||||
@@ -21,8 +21,8 @@ class SequenceMixin(models.AbstractModel):
|
||||
_sequence_field = "name"
|
||||
_sequence_date_field = "date"
|
||||
_sequence_index = False
|
||||
_sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((20|21)\d{2}|(\d{2}(?=\D))))(?P<prefix2>\D*?)(?P<month>(0[1-9]|1[0-2]))(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((20|21)?\d{2}))(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((19|20|21)\d{2}|(\d{2}(?=\D))))(?P<prefix2>\D*?)(?P<month>(0[1-9]|1[0-2]))(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((19|20|21)?\d{2}))(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_fixed_regex = r'^(?P<prefix1>.*?)(?P<seq>\d{0,9})(?P<suffix>\D*?)$'
|
||||
|
||||
sequence_prefix = fields.Char(compute='_compute_split_sequence', store=True)
|
||||
|
||||
@@ -28,43 +28,63 @@ flectra.define('account.tax_group', function (require) {
|
||||
* @param {Id} taxGroupId
|
||||
* @param {Float} deltaAmount
|
||||
*/
|
||||
_changeTaxValueByTaxGroup: function (taxGroupId, deltaAmount) {
|
||||
var self = this;
|
||||
// Search for the first tax line with the same tax group and modify its value
|
||||
var line_id = self.record.data.line_ids.data.find(elem => elem.data.tax_group_id && elem.data.tax_group_id.data.id === taxGroupId);
|
||||
_changeTaxValueByTaxGroup: function (taxGroupId, deltaAmount) {
|
||||
const self = this;
|
||||
|
||||
var debitAmount = 0;
|
||||
var creditAmount = 0;
|
||||
var amount_currency = 0;
|
||||
if (line_id.data.currency_id) { // If multi currency enable
|
||||
if (this.record.data.move_type === "in_invoice") {
|
||||
amount_currency = line_id.data.amount_currency - deltaAmount;
|
||||
// Search for the first tax line with the same tax group and modify its value
|
||||
function applyChange(line_id){
|
||||
let debitAmount = 0;
|
||||
let creditAmount = 0;
|
||||
let amount_currency = 0;
|
||||
if (line_id.data.currency_id) { // If multi currency enable
|
||||
if (self.record.data.move_type === "in_invoice") {
|
||||
amount_currency = line_id.data.amount_currency - deltaAmount;
|
||||
} else {
|
||||
amount_currency = line_id.data.amount_currency + deltaAmount;
|
||||
}
|
||||
} else {
|
||||
amount_currency = line_id.data.amount_currency + deltaAmount;
|
||||
}
|
||||
} else {
|
||||
var balance = line_id.data.price_subtotal;
|
||||
balance -= deltaAmount;
|
||||
if (this.record.data.move_type === "in_invoice") { // For vendor bill
|
||||
if (balance > 0) {
|
||||
debitAmount = balance;
|
||||
} else if (balance < 0) {
|
||||
creditAmount = -balance;
|
||||
}
|
||||
} else { // For refund
|
||||
if (balance > 0) {
|
||||
creditAmount = balance;
|
||||
} else if (balance < 0) {
|
||||
debitAmount = -balance;
|
||||
let balance = line_id.data.price_subtotal;
|
||||
balance -= deltaAmount;
|
||||
if (self.record.data.move_type === "in_invoice") { // For vendor bill
|
||||
if (balance > 0) {
|
||||
debitAmount = balance;
|
||||
} else if (balance < 0) {
|
||||
creditAmount = -balance;
|
||||
}
|
||||
} else { // For refund
|
||||
if (balance > 0) {
|
||||
creditAmount = balance;
|
||||
} else if (balance < 0) {
|
||||
debitAmount = -balance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger ORM
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: self.record.id,
|
||||
changes: { line_ids: { operation: "UPDATE", id: line_id.id, data: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount } } }, // account.move change
|
||||
initialEvent: { dataPointID: line_id.id, changes: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount }, }, // account.move.line change
|
||||
});
|
||||
}
|
||||
|
||||
let line_id = self.record.data.line_ids.data.find(elem => elem.data.tax_group_id && elem.data.tax_group_id.data.id === taxGroupId);
|
||||
|
||||
if (line_id){
|
||||
applyChange(line_id);
|
||||
} else {
|
||||
const {limit, id, count} = self.record.data.line_ids;
|
||||
let offset = count - limit;
|
||||
self.trigger_up('load', {
|
||||
id,
|
||||
limit,
|
||||
offset,
|
||||
on_success: value => {
|
||||
line_id = value.data.find(elem => elem.data.tax_group_id && elem.data.tax_group_id.data.id === taxGroupId);
|
||||
applyChange(line_id);
|
||||
},
|
||||
});
|
||||
}
|
||||
// Trigger ORM
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: self.record.id,
|
||||
changes: { line_ids: { operation: "UPDATE", id: line_id.id, data: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount } } }, // account.move change
|
||||
initialEvent: { dataPointID: line_id.id, changes: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount }, }, // account.move.line change
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ class FleetVehicle(models.Model):
|
||||
return
|
||||
|
||||
for vehicle in self:
|
||||
vehicle.account_move_ids = self.env['account.move.line'].search([('vehicle_id', '=', self.id), ('move_id.state', '!=', 'cancel')]).move_id
|
||||
vehicle.account_move_ids = self.env['account.move.line'].search([('vehicle_id', '=', vehicle.id), ('move_id.state', '!=', 'cancel')]).move_id
|
||||
vehicle.bill_count = len(vehicle.account_move_ids)
|
||||
|
||||
def action_view_bills(self):
|
||||
|
||||
@@ -18,7 +18,7 @@ class SaleOrder(models.Model):
|
||||
@api.depends('order_line')
|
||||
def _compute_is_service_products(self):
|
||||
for so in self:
|
||||
so.is_all_service = all(line.product_id.type == 'service' for line in so.order_line)
|
||||
so.is_all_service = all(line.product_id.type == 'service' for line in so.order_line.filtered(lambda x: not x.display_type))
|
||||
|
||||
def _compute_amount_total_without_delivery(self):
|
||||
self.ensure_one()
|
||||
@@ -30,8 +30,9 @@ class SaleOrder(models.Model):
|
||||
for order in self:
|
||||
order.delivery_set = any(line.is_delivery for line in order.order_line)
|
||||
|
||||
@api.onchange('order_line', 'partner_id')
|
||||
@api.onchange('order_line', 'partner_id', 'partner_shipping_id')
|
||||
def onchange_order_line(self):
|
||||
self.ensure_one()
|
||||
delivery_line = self.order_line.filtered('is_delivery')
|
||||
if delivery_line:
|
||||
self.recompute_delivery_price = True
|
||||
|
||||
@@ -41,6 +41,7 @@ class StockMoveLine(models.Model):
|
||||
unit_price = move_line.product_id.list_price
|
||||
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id)
|
||||
move_line.sale_price = unit_price * qty
|
||||
super(StockMoveLine, self)._compute_sale_price()
|
||||
|
||||
def _get_aggregated_product_quantities(self, **kwargs):
|
||||
"""Returns dictionary of products and corresponding values of interest + hs_code
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<t t-set="checked_in" t-value="widget.employee.attendance_state=='checked_in'"/>
|
||||
<t t-if="widget.employee">
|
||||
<div class="o_hr_attendance_user_badge o_home_menu_background">
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.employee.id}" t-att-title="widget.employee.name" t-att-alt="widget.employee.name"/>
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee.public&field=image_128&id=#{widget.employee.id}" t-att-title="widget.employee.name" t-att-alt="widget.employee.name"/>
|
||||
</div>
|
||||
<h1 class="mb8"><t t-esc="widget.employee.name"/></h1>
|
||||
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
<t t-if="widget.employee_id">
|
||||
<div class="o_hr_attendance_user_badge o_home_menu_background">
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.employee_id}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/>
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee.public&field=image_128&id=#{widget.employee_id}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/>
|
||||
</div>
|
||||
<h1 class="mb8"><t t-esc="widget.employee_name"/></h1>
|
||||
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
@@ -109,7 +109,7 @@
|
||||
<div class="o_hr_attendance_kiosk_mode">
|
||||
<t t-if="widget.attendance">
|
||||
<div class="o_hr_attendance_user_badge o_home_menu_background">
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.attendance.employee_id[0]}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/>
|
||||
<img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee.public&field=image_128&id=#{widget.attendance.employee_id[0]}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/>
|
||||
</div>
|
||||
<t t-if="widget.attendance.check_out">
|
||||
<h1 class="mb0">Goodbye <t t-esc="widget.employee_name"/>!</h1>
|
||||
|
||||
@@ -137,6 +137,12 @@
|
||||
<field name="domain_force">['|', ('holiday_status_id.company_id', '=', False), ('holiday_status_id.company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_allocation_rule_multicompany" model="ir.rule">
|
||||
<field name="name">Leave Allocations: multi company global rule</field>
|
||||
<field name="model_id" ref="model_hr_leave_allocation"/>
|
||||
<field name="domain_force">['|', ('holiday_status_id.company_id', '=', False), ('holiday_status_id.company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_allocation_rule_employee" model="ir.rule">
|
||||
<field name="name">Allocations: employee: read own</field>
|
||||
<field name="model_id" ref="model_hr_leave_allocation"/>
|
||||
|
||||
@@ -420,6 +420,9 @@ class Applicant(models.Model):
|
||||
# want the gateway user to be responsible if no other responsible is
|
||||
# found.
|
||||
self = self.with_context(default_user_id=False)
|
||||
stage = False
|
||||
if custom_values and 'job_id' in custom_values:
|
||||
stage = self.env['hr.job'].browse(custom_values['job_id'])._get_first_stage()
|
||||
val = msg.get('from').split('<')[0]
|
||||
defaults = {
|
||||
'name': msg.get('subject') or _("No Subject"),
|
||||
@@ -429,6 +432,8 @@ class Applicant(models.Model):
|
||||
}
|
||||
if msg.get('priority'):
|
||||
defaults['priority'] = msg.get('priority')
|
||||
if stage and stage.id:
|
||||
defaults['stage_id'] = stage.id
|
||||
if custom_values:
|
||||
defaults.update(custom_values)
|
||||
return super(Applicant, self).message_new(msg, custom_values=defaults)
|
||||
|
||||
@@ -97,13 +97,14 @@ class IapTransaction(object):
|
||||
self.credit = None
|
||||
|
||||
|
||||
def iap_authorize(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None):
|
||||
def iap_authorize(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None, ttl=4320):
|
||||
endpoint = iap_get_endpoint(env)
|
||||
params = {
|
||||
'account_token': account_token,
|
||||
'credit': credit,
|
||||
'key': key,
|
||||
'description': description,
|
||||
'ttl': ttl
|
||||
}
|
||||
if dbuuid:
|
||||
params.update({'dbuuid': dbuuid})
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<field name="inherit_id" ref="account.view_tax_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field position="after" name="price_include">
|
||||
<field name="tax_discount"/>
|
||||
<field name="tax_discount" attrs="{'invisible': [('country_code', '!=', 'BR')]}"/>
|
||||
</field>
|
||||
<field position="after" name="tax_discount">
|
||||
<field name="base_reduction" widget="monetary"/>
|
||||
<field name="amount_mva" widget="monetary"/>
|
||||
<field name="base_reduction" widget="monetary" attrs="{'invisible': [('country_code', '!=', 'BR')]}"/>
|
||||
<field name="amount_mva" widget="monetary" attrs="{'invisible': [('country_code', '!=', 'BR')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -54,12 +54,13 @@ class ResPartner(models.Model):
|
||||
return super().create(values)
|
||||
|
||||
def write(self, values):
|
||||
for record in self:
|
||||
vat_values = {
|
||||
'vat': values.get('vat', record.vat),
|
||||
'l10n_latam_identification_type_id': values.get(
|
||||
'l10n_latam_identification_type_id', record.l10n_latam_identification_type_id.id),
|
||||
'country_id': values.get('country_id', record.country_id.id)
|
||||
}
|
||||
values['vat'] = self._format_vat_cl(vat_values)
|
||||
if 'vat' in values or 'l10n_latam_identification_type_id' in values:
|
||||
for record in self:
|
||||
vat_values = {
|
||||
'vat': values.get('vat', record.vat),
|
||||
'l10n_latam_identification_type_id': values.get(
|
||||
'l10n_latam_identification_type_id', record.l10n_latam_identification_type_id.id),
|
||||
'country_id': values.get('country_id', record.country_id.id)
|
||||
}
|
||||
values['vat'] = self._format_vat_cl(vat_values)
|
||||
return super().write(values)
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="inherit_id" ref="account.view_tax_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="l10n_cl_sii_code" options="{'format': false}"/>
|
||||
<field name="l10n_cl_sii_code" options="{'format': false}" attrs="{'invisible': [('country_code', '!=', 'CL')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="inherit_id" ref="account.view_tax_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="include_base_amount" position="after">
|
||||
<field name="l10n_de_datev_code"/>
|
||||
<field name="l10n_de_datev_code" attrs="{'invisible': [('country_code', '!=', 'DE')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -9,8 +9,11 @@ class AccountBankStatement(models.Model):
|
||||
_inherit = 'account.bank.statement'
|
||||
|
||||
def unlink(self):
|
||||
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.pos_payment_method_ids):
|
||||
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,))
|
||||
for statement in self:
|
||||
if not statement.company_id._is_accounting_unalterable() or not statement.s.journal_id.pos_payment_method_ids:
|
||||
continue
|
||||
if statement.state != 'open':
|
||||
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name))
|
||||
return super(AccountBankStatement, self).unlink()
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="inherit_id" ref="account.view_tax_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="include_base_amount" position="after">
|
||||
<field name="l10n_in_reverse_charge" attrs="{'invisible':[('amount_type','=', 'group')]}"/>
|
||||
<field name="l10n_in_reverse_charge" attrs="{'invisible':['|', ('amount_type','=', 'group'), ('country_code', '!=', 'IN')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<xpath expr="//page[@name='other_info']" position="after">
|
||||
<page string="Electronic Invoicing"
|
||||
name="electronic_invoicing"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}">
|
||||
attrs="{'invisible': ['|', ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')), ('country_code', '!=', 'IT')]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="l10n_it_stamp_duty"/>
|
||||
|
||||
@@ -806,33 +806,33 @@ chart5911,5911,Utilidades no distribuidas - Utilidades acumuladas,account.data_a
|
||||
chart5912,5912,Utilidades no distribuidas - Ingresos de años anteriores,account.data_account_type_equity,pe_chart_template,False
|
||||
chart5921,5921,Pérdidas acumuladas - Pérdidas acumuladas,account.data_account_type_equity,pe_chart_template,False
|
||||
chart5922,5922,Pérdidas acumuladas - Gastos de años anteriores,account.data_account_type_equity,pe_chart_template,False
|
||||
chart6011,6011,Mercaderías - Mercaderías,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart602,602,Materias primas,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6031,6031,"Materiales auxiliares, suministros y repuestos - Materiales auxiliares",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6032,6032,"Materiales auxiliares, suministros y repuestos - Suministros",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6033,6033,"Materiales auxiliares, suministros y repuestos - Repuestos",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6041,6041,Envases y embalajes - Envases,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6042,6042,Envases y embalajes - Embalajes,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60911,60911,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Transporte,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60912,60912,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Seguros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60913,60913,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Derechos aduaneros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60914,60914,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Comisiones,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60919,60919,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Otros costos,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60921,60921,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Transporte,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60922,60922,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Seguros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60923,60923,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Derechos aduaneros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60924,60924,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Comisiones,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60925,60925,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Otros costos,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60931,60931,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Transporte",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60932,60932,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Seguros",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60933,60933,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Derechos aduaneros",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60934,60934,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Comisiones",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60935,60935,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Otros costos",account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60941,60941,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Transporte,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60942,60942,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Seguros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60943,60943,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Derechos aduaneros,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60944,60944,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Comisiones,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart60945,60945,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Otros costos,account.data_account_type_current_assets,pe_chart_template,False
|
||||
chart6011,6011,Mercaderías - Mercaderías,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart602,602,Materias primas,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6031,6031,"Materiales auxiliares, suministros y repuestos - Materiales auxiliares",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6032,6032,"Materiales auxiliares, suministros y repuestos - Suministros",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6033,6033,"Materiales auxiliares, suministros y repuestos - Repuestos",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6041,6041,Envases y embalajes - Envases,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6042,6042,Envases y embalajes - Embalajes,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60911,60911,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Transporte,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60912,60912,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Seguros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60913,60913,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Derechos aduaneros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60914,60914,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Comisiones,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60919,60919,Costos vinculados con las compras - Costos vinculados con las compras de mercaderías - Otros costos,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60921,60921,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Transporte,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60922,60922,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Seguros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60923,60923,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Derechos aduaneros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60924,60924,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Comisiones,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60925,60925,Costos vinculados con las compras - Costos vinculados con las compras de materias primas - Otros costos,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60931,60931,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Transporte",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60932,60932,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Seguros",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60933,60933,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Derechos aduaneros",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60934,60934,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Comisiones",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60935,60935,"Costos vinculados con las compras - Costos vinculados con las compras de materiales, suministros y repuestos - Otros costos",account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60941,60941,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Transporte,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60942,60942,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Seguros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60943,60943,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Derechos aduaneros,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60944,60944,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Comisiones,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart60945,60945,Costos vinculados con las compras - Costos vinculados con las compras de envases y embalajes - Otros costos,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6111,6111,Mercaderías - Mercaderías,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6121,6121,Materias primas - Materias primas,account.data_account_type_expenses,pe_chart_template,False
|
||||
chart6131,6131,"Materiales auxiliares, suministros y repuestos - Materiales auxiliares",account.data_account_type_expenses,pe_chart_template,False
|
||||
|
||||
|
@@ -1994,8 +1994,8 @@ class MailThread(models.AbstractModel):
|
||||
MailThread = self.env['mail.thread']
|
||||
values = {
|
||||
'parent_id': parent_id,
|
||||
'model': self._name if self else False,
|
||||
'res_id': self.id if self else False,
|
||||
'model': self._name if self else model,
|
||||
'res_id': self.id if self else res_id,
|
||||
'message_type': 'user_notification',
|
||||
'subject': subject,
|
||||
'body': body,
|
||||
|
||||
@@ -320,7 +320,8 @@ const DiscussWidget = AbstractAction.extend({
|
||||
* @private
|
||||
*/
|
||||
_onClickMarkAllAsRead() {
|
||||
this.env.models['mail.message'].markAllAsRead(this.domain);
|
||||
const domain = this.discuss.stringifiedDomain ? JSON.parse(this.discuss.stringifiedDomain) : undefined;
|
||||
this.env.models['mail.message'].markAllAsRead(domain);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -571,8 +571,11 @@ class MassMailing(models.Model):
|
||||
def action_send_mail(self, res_ids=None):
|
||||
author_id = self.env.user.partner_id.id
|
||||
|
||||
# If no recipient is passed, we don't want to use the recipients of the first
|
||||
# mailing for all the others
|
||||
initial_res_ids = res_ids
|
||||
for mailing in self:
|
||||
if not res_ids:
|
||||
if not initial_res_ids:
|
||||
res_ids = mailing._get_remaining_recipients()
|
||||
if not res_ids:
|
||||
raise UserError(_('There are no recipients selected.'))
|
||||
|
||||
@@ -808,7 +808,10 @@ class MrpProduction(models.Model):
|
||||
# covers at least 2 cases: backorders generation (follow default logic for moves copying)
|
||||
# and copying a done MO via the form (i.e. copy only the non-cancelled moves since no backorder = cancelled finished moves)
|
||||
if not default or 'move_finished_ids' not in default:
|
||||
default['move_finished_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_finished_ids.filtered(lambda m: m.state != 'cancel' and m.product_qty != 0.0)]
|
||||
move_finished_ids = self.move_finished_ids
|
||||
if self.state != 'cancel':
|
||||
move_finished_ids = self.move_finished_ids.filtered(lambda m: m.state != 'cancel' and m.product_qty != 0.0)
|
||||
default['move_finished_ids'] = [(0, 0, move.copy_data()[0]) for move in move_finished_ids]
|
||||
if not default or 'move_raw_ids' not in default:
|
||||
default['move_raw_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_raw_ids.filtered(lambda m: m.product_qty != 0.0)]
|
||||
return super(MrpProduction, self).copy_data(default=default)
|
||||
|
||||
@@ -127,6 +127,8 @@ class ProductProduct(models.Model):
|
||||
bom_kits = self.env['mrp.bom']._get_product2bom(self, bom_type='phantom')
|
||||
kits = self.filtered(lambda p: bom_kits.get(p))
|
||||
res = super(ProductProduct, self - kits)._compute_quantities_dict(lot_id, owner_id, package_id, from_date=from_date, to_date=to_date)
|
||||
qties = self.env.context.get("mrp_compute_quantities", {})
|
||||
qties.update(res)
|
||||
for product in bom_kits:
|
||||
boms, bom_sub_lines = bom_kits[product].explode(product, 1)
|
||||
ratios_virtual_available = []
|
||||
@@ -135,7 +137,7 @@ class ProductProduct(models.Model):
|
||||
ratios_outgoing_qty = []
|
||||
ratios_free_qty = []
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
component = bom_line.product_id
|
||||
component = bom_line.product_id.with_context(mrp_compute_quantities=qties)
|
||||
if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
|
||||
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
|
||||
# to avoid a division by zero. The same logic is applied to non-storable products as those
|
||||
@@ -145,7 +147,7 @@ class ProductProduct(models.Model):
|
||||
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, raise_if_failure=False)
|
||||
if not qty_per_kit:
|
||||
continue
|
||||
component_res = res.get(component.id, {
|
||||
component_res = qties.get(component.id, {
|
||||
"virtual_available": component.virtual_available,
|
||||
"qty_available": component.qty_available,
|
||||
"incoming_qty": component.incoming_qty,
|
||||
|
||||
@@ -114,6 +114,15 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(len(mo_copy.move_finished_ids), 1, "Incorrect number of moves for products to produce [i.e. cancelled moves should not be copied")
|
||||
self.assertEqual(mo_copy.move_finished_ids.product_uom_qty, 2, "Incorrect qty of products to produce")
|
||||
|
||||
# check that a cancelled MO is copied correctly
|
||||
mo_copy.action_cancel()
|
||||
self.assertEqual(mo_copy.state, 'cancel')
|
||||
mo_copy_2 = mo_copy.copy()
|
||||
self.assertEqual(mo_copy_2.state, 'draft', "Copied production order should be draft.")
|
||||
self.assertEqual(len(mo_copy_2.move_raw_ids), 2, "Incorrect number of component moves.")
|
||||
self.assertEqual(len(mo_copy_2.move_finished_ids), 1, "Incorrect number of moves for products to produce [i.e. copying a cancelled MO should copy its cancelled moves]")
|
||||
self.assertEqual(mo_copy_2.move_finished_ids.product_uom_qty, 2, "Incorrect qty of products to produce")
|
||||
|
||||
def test_production_avialability(self):
|
||||
""" Checks the availability of a production order through mutliple calls to `action_assign`.
|
||||
"""
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
decoration-success="not is_done and (quantity_done - should_consume_qty == 0)"
|
||||
decoration-warning="not is_done and (quantity_done - should_consume_qty > 0.0001)"
|
||||
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('show_details_visible', '=', True)]}"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="show_details_visible" invisible="1"/>
|
||||
<field name="lot_ids" widget="many2many_tags"
|
||||
groups="stock.group_production_lot" optional="hide"
|
||||
|
||||
@@ -112,15 +112,15 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
|
||||
_adyen_pay: function () {
|
||||
var self = this;
|
||||
var order = this.pos.get_order();
|
||||
|
||||
if (this.pos.get_order().selected_paymentline.amount < 0) {
|
||||
if (order.selected_paymentline.amount < 0) {
|
||||
this._show_error(_t('Cannot process transactions with negative amount.'));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
if (this.poll_response_error) {
|
||||
this.poll_response_error = false;
|
||||
if (order === this.poll_error_order) {
|
||||
delete this.poll_error_order;
|
||||
return self._adyen_handle_response({});
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
shadow: true,
|
||||
}).catch(function (data) {
|
||||
reject();
|
||||
self.poll_response_error = true;
|
||||
self.poll_error_order = self.pos.get_order();
|
||||
return self._handle_flectra_connection_failure(data);
|
||||
}).then(function (status) {
|
||||
var notification = status.latest_response;
|
||||
|
||||
@@ -444,7 +444,7 @@ class PurchaseOrderLine(models.Model):
|
||||
|
||||
def _check_orderpoint_picking_type(self):
|
||||
warehouse_loc = self.order_id.picking_type_id.warehouse_id.view_location_id
|
||||
if self.orderpoint_id and not warehouse_loc.parent_path in self.orderpoint_id.location_id.parent_path:
|
||||
if warehouse_loc and self.orderpoint_id and not warehouse_loc.parent_path in self.orderpoint_id.location_id.parent_path:
|
||||
raise UserError(_('For the product %s, the warehouse of the operation type (%s) is inconsistent with the location (%s) of the reordering rule (%s). Change the operation type or cancel the request for quotation.',
|
||||
self.product_id.display_name, self.order_id.picking_type_id.display_name, self.orderpoint_id.location_id.display_name, self.orderpoint_id.display_name))
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
</group>
|
||||
<group attrs="{'invisible': [('action','!=','exist')], 'required':[('action', '=','exist')]}">
|
||||
<group>
|
||||
<field name="partner_id" attrs="{'invisible': [('action','!=','exist')], 'required':[('action', '=','exist')]}"/>
|
||||
<field name="partner_id" attrs="{'invisible': [('action','!=','exist')], 'required':[('action', '=','exist')]}"
|
||||
context="{'res_partner_search_mode': 'customer'}" />
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import sale
|
||||
from . import account_move
|
||||
from . import mrp_production
|
||||
from . import stock_move
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import api, fields, models
|
||||
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = 'stock.move.line'
|
||||
|
||||
def _compute_sale_price(self):
|
||||
kit_lines = self.filtered(lambda move_line: move_line.move_id.bom_line_id.bom_id.type == 'phantom')
|
||||
for move_line in kit_lines:
|
||||
unit_price = move_line.product_id.list_price
|
||||
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id)
|
||||
move_line.sale_price = unit_price * qty
|
||||
super(StockMoveLine, self - kit_lines)._compute_sale_price()
|
||||
@@ -76,7 +76,7 @@ class SaleOrder(models.Model):
|
||||
for order in self:
|
||||
pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer')
|
||||
dates_list = [date for date in pickings.mapped('date_done') if date]
|
||||
order.effective_date = min(dates_list).date() if dates_list else False
|
||||
order.effective_date = fields.Date.context_today(order, min(dates_list)) if dates_list else False
|
||||
|
||||
@api.depends('picking_policy')
|
||||
def _compute_expected_date(self):
|
||||
|
||||
@@ -86,7 +86,7 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
|
||||
ml.qty_done = ml.product_uom_qty
|
||||
picking._action_done()
|
||||
self.assertEqual(picking.state, 'done', "Picking not processed correctly!")
|
||||
self.assertEqual(fields.Date.today(), sale_order.effective_date, "Wrong effective date on sale order!")
|
||||
self.assertEqual(fields.Date.context_today(sale_order), sale_order.effective_date, "Wrong effective date on sale order!")
|
||||
|
||||
def test_sale_order_commitment_date(self):
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_module
|
||||
from . import sale_order_line
|
||||
@@ -0,0 +1,25 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra import api, models
|
||||
|
||||
|
||||
class IrModule(models.Model):
|
||||
_inherit = 'ir.module.module'
|
||||
|
||||
|
||||
@api.returns('self')
|
||||
def downstream_dependencies(
|
||||
self,
|
||||
known_deps=None,
|
||||
exclude_states=('uninstalled', 'uninstallable', 'to remove'),
|
||||
):
|
||||
# sale_stock_margin implicitly depends on sale_stock, but sale_stock is not marked as one of
|
||||
# its dependencies, thus uninstalling sale_stock without uninstalling sale_stock_margin
|
||||
# will make the registry crash, the install works purely because sale_stock is auto-installed
|
||||
# when the dependencies of sale_stock_margin are installed
|
||||
if 'sale_stock' in self.mapped('name'):
|
||||
# we force sale_stock_margin as a dependant of sale_stock
|
||||
known_deps = (known_deps or self.browse()) | self.search([
|
||||
('name', '=', 'sale_stock_margin'),
|
||||
('state', '=', 'installed'),
|
||||
], limit=1)
|
||||
return super().downstream_dependencies(known_deps, exclude_states)
|
||||
@@ -199,10 +199,15 @@ class SaleOrderLine(models.Model):
|
||||
"""
|
||||
lines_by_timesheet = self.filtered(lambda sol: sol.product_id and sol.product_id._is_delivered_timesheet())
|
||||
domain = lines_by_timesheet._timesheet_compute_delivered_quantity_domain()
|
||||
domain = expression.AND([domain, [
|
||||
refund_account_moves = self.order_id.invoice_ids.filtered(lambda am: am.state == 'posted' and am.move_type == 'out_refund').reversed_entry_id
|
||||
timesheet_domain = [
|
||||
'|',
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_id.state', '=', 'cancel')]])
|
||||
('timesheet_invoice_id.state', '=', 'cancel')]
|
||||
if refund_account_moves:
|
||||
credited_timesheet_domain = [('timesheet_invoice_id.state', '=', 'posted'), ('timesheet_invoice_id', 'in', refund_account_moves.ids)]
|
||||
timesheet_domain = expression.OR([timesheet_domain, credited_timesheet_domain])
|
||||
domain = expression.AND([domain, timesheet_domain])
|
||||
if start_date:
|
||||
domain = expression.AND([domain, [('date', '>=', start_date)]])
|
||||
if end_date:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import timedelta
|
||||
|
||||
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.fields import Date
|
||||
from flectra.tests import Form, tagged
|
||||
|
||||
|
||||
@@ -283,3 +287,90 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
||||
self.assertEqual(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill")
|
||||
self.assertEqual(sale_order_line.qty_delivered, 1, "The delivered quantity of SO line should not have been incremented")
|
||||
self.assertTrue(invoice_a.mapped('line_ids.analytic_line_ids'), "Analytic lines should be generated")
|
||||
|
||||
def test_reversed_invoice_reinvoice_with_period(self):
|
||||
"""
|
||||
Tests that when reversing an invoice of timesheet and selecting a time
|
||||
period, the qty to invoice is correctly found
|
||||
Business flow:
|
||||
Create a sale order and deliver some hours (invoiced = 0)
|
||||
Create an invoice
|
||||
Confirm (invoiced = 1)
|
||||
Add Credit Note
|
||||
Confirm (invoiced = 0)
|
||||
Go back to the SO
|
||||
Create an invoice
|
||||
Select a time period [1 week ago, 1 week in the future]
|
||||
Confirm
|
||||
-> Fails if there is nothing to invoice
|
||||
"""
|
||||
product = self.env['product.product'].create({
|
||||
'name': "Service delivered, create task in global project",
|
||||
'standard_price': 30,
|
||||
'list_price': 90,
|
||||
'type': 'service',
|
||||
'service_policy': 'delivered_timesheet',
|
||||
'invoice_policy': 'delivery',
|
||||
'default_code': 'SERV-DELI2',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': self.project_global.id,
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': self.account_sale.id,
|
||||
})
|
||||
today = Date.context_today(self.env.user)
|
||||
|
||||
# Creates a sales order for quantity 3
|
||||
so_form = Form(self.env['sale.order'])
|
||||
so_form.partner_id = self.env['res.partner'].create({'name': 'Toto'})
|
||||
with so_form.order_line.new() as line:
|
||||
line.product_id = product
|
||||
line.product_uom_qty = 3.0
|
||||
sale_order = so_form.save()
|
||||
sale_order.action_confirm()
|
||||
|
||||
# "Deliver" 1 of 3
|
||||
task = sale_order.tasks_ids
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_user.id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
context = {
|
||||
"active_model": 'sale.order',
|
||||
"active_ids": [sale_order.id],
|
||||
"active_id": sale_order.id,
|
||||
'open_invoices': True,
|
||||
}
|
||||
# Invoice the 1
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'delivered'
|
||||
})
|
||||
invoice_dict = wizard.create_invoices()
|
||||
# Confirm the invoice
|
||||
invoice = self.env['account.move'].browse(invoice_dict['res_id'])
|
||||
invoice.action_post()
|
||||
# Refund the invoice
|
||||
refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice.id]).create({
|
||||
'reason': 'please reverse :c',
|
||||
'refund_method': 'refund',
|
||||
'date': today,
|
||||
})
|
||||
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
|
||||
refund_invoice.action_post()
|
||||
# reversing with action_reverse and then action_post does not reset the invoice_status to 'to invoice' in tests
|
||||
|
||||
# Recreate wizard to get the new invoices created
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
'date_start_invoice_timesheet': today - timedelta(days=7),
|
||||
'date_end_invoice_timesheet': today + timedelta(days=7)
|
||||
})
|
||||
|
||||
# The actual test :
|
||||
wizard.create_invoices() # No exception should be raised, there is indeed something to be invoiced since it was reversed
|
||||
|
||||
@@ -867,8 +867,10 @@ class StockMove(models.Model):
|
||||
'confirmed': 1,
|
||||
}
|
||||
moves_todo = self\
|
||||
.filtered(lambda move: move.state not in ['cancel', 'done'])\
|
||||
.filtered(lambda move: move.state not in ['cancel', 'done'] and not (move.state == 'assigned' and not move.product_uom_qty))\
|
||||
.sorted(key=lambda move: (sort_map.get(move.state, 0), move.product_uom_qty))
|
||||
if not moves_todo:
|
||||
return 'assigned'
|
||||
# The picking should be the same for all moves.
|
||||
if moves_todo[:1].picking_id and moves_todo[:1].picking_id.move_type == 'one':
|
||||
most_important_move = moves_todo[0]
|
||||
|
||||
@@ -674,3 +674,7 @@ class StockMoveLine(models.Model):
|
||||
else:
|
||||
aggregated_move_lines[line_key]['qty_done'] += move_line.qty_done
|
||||
return aggregated_move_lines
|
||||
|
||||
def _compute_sale_price(self):
|
||||
# To Override
|
||||
pass
|
||||
@@ -799,3 +799,34 @@ class TestPacking(TestPackingCommon):
|
||||
with Form(picking) as picking_form:
|
||||
picking_form.package_level_ids.remove(0)
|
||||
self.assertEqual(len(picking.move_lines), 1, 'Should have only 1 stock move')
|
||||
|
||||
def test_picking_state_with_null_qty(self):
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(default_immediate_transfer=False))
|
||||
picking_type_id = self.warehouse.out_type_id
|
||||
receipt_form.picking_type_id = picking_type_id
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.productA
|
||||
move_line.product_uom_qty = 10
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.productB
|
||||
move_line.product_uom_qty = 10
|
||||
receipt = receipt_form.save()
|
||||
receipt.action_confirm()
|
||||
self.assertEqual(receipt.state, 'confirmed')
|
||||
receipt.move_ids_without_package[1].product_uom_qty = 0
|
||||
self.assertEqual(receipt.state, 'confirmed')
|
||||
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(default_immediate_transfer=True))
|
||||
picking_type_id = self.warehouse.out_type_id
|
||||
receipt_form.picking_type_id = picking_type_id
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.productA
|
||||
move_line.quantity_done = 10
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.productB
|
||||
move_line.quantity_done = 10
|
||||
receipt = receipt_form.save()
|
||||
receipt.action_confirm()
|
||||
self.assertEqual(receipt.state, 'assigned')
|
||||
receipt.move_ids_without_package[1].product_uom_qty = 0
|
||||
self.assertEqual(receipt.state, 'assigned')
|
||||
@@ -51,6 +51,7 @@
|
||||
<field name="json_lead_days_popover" optional="show" nolabel="1" string="Forecast Description" width="10px" widget="popover_widget" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<field name="route_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="trigger" optional="hide"/>
|
||||
<field name="group_id" optional="hide" groups="stock.group_adv_location"/>
|
||||
<field name="product_min_qty" optional="show"/>
|
||||
<field name="product_max_qty" optional="show"/>
|
||||
<field name="qty_multiple" optional="hide"/>
|
||||
@@ -80,6 +81,7 @@
|
||||
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="route_id" optional="hide"/>
|
||||
<field name="trigger" optional="hide"/>
|
||||
<field name="group_id" optional="hide" groups="stock.group_adv_location"/>
|
||||
<field name="product_min_qty" optional="show"/>
|
||||
<field name="product_max_qty" optional="show"/>
|
||||
<field name="qty_multiple" optional="show"/>
|
||||
@@ -99,6 +101,7 @@
|
||||
<field name="product_id"/>
|
||||
<field name="trigger"/>
|
||||
<field name="product_category_id"/>
|
||||
<field name="group_id" groups="stock.group_adv_location"/>
|
||||
<field name="warehouse_id" groups="stock.group_stock_multi_warehouses"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<filter string="To Reorder" name="filter_to_reorder" domain="[('qty_to_order', '>', 0.0)]"/>
|
||||
@@ -123,6 +126,7 @@
|
||||
<field name="name" string="Reordering Rule"/>
|
||||
<field name="product_id"/>
|
||||
<field name="trigger"/>
|
||||
<field name="group_id" groups="stock.group_adv_location"/>
|
||||
<field name="warehouse_id" groups="stock.group_stock_multi_warehouses"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
|
||||
@@ -220,6 +220,7 @@ class AccountMoveLine(models.Model):
|
||||
# OVERRIDE to use the stock input account by default on vendor bills when dealing
|
||||
# with anglo-saxon accounting.
|
||||
self.ensure_one()
|
||||
self = self.with_company(self.move_id.journal_id.company_id)
|
||||
if self.product_id.type == 'product' \
|
||||
and self.move_id.company_id.anglo_saxon_accounting \
|
||||
and self.move_id.is_purchase_document():
|
||||
|
||||
@@ -199,7 +199,7 @@ class StockLandedCost(models.Model):
|
||||
|
||||
for move in self._get_targeted_move_ids():
|
||||
# it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
|
||||
if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average') or move.state == 'cancel':
|
||||
if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average') or move.state == 'cancel' or not move.product_qty:
|
||||
continue
|
||||
vals = {
|
||||
'product_id': move.product_id.id,
|
||||
|
||||
@@ -438,7 +438,8 @@ class SurveyQuestion(models.Model):
|
||||
|
||||
count_data = dict.fromkeys(suggested_answers, 0)
|
||||
for line in user_input_lines:
|
||||
if line.suggested_answer_id or (line.value_char_box and self.comment_count_as_answer):
|
||||
if line.suggested_answer_id in count_data\
|
||||
or (line.value_char_box and self.comment_count_as_answer):
|
||||
count_data[line.suggested_answer_id] += 1
|
||||
|
||||
table_data = [{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div t-if="user_input.test_entry" class="test-entry"/>
|
||||
<div class="certification-top">
|
||||
<div class="certification-bg-dark">
|
||||
<div class="certification-company-wrapper" t-field="user_input.survey_id.create_uid.company_id.logo" t-options="{'widget': 'image'}" role="img"/>
|
||||
<div class="certification-company-wrapper" t-field="user_input.env.company.logo" t-options="{'widget': 'image'}" role="img"/>
|
||||
</div>
|
||||
<div class="certification-name" t-field="user_input.survey_id.display_name"/>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="user-name" t-esc="user_input.partner_id.name or user_input.email"/>
|
||||
|
||||
<div class="certification-description">
|
||||
<span t-field="user_input.survey_id.create_uid.company_id.display_name"/> is proud to award this certificate to
|
||||
<span t-field="user_input.env.company.display_name"/> is proud to award this certificate to
|
||||
<span t-esc="user_input.partner_id.name or user_input.email"/> for graduating <span t-field="user_input.survey_id.display_name"/>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
<hr class="small"/>
|
||||
|
||||
<div class="certification-description">
|
||||
<span t-field="user_input.survey_id.create_uid.company_id.display_name"/> is proud to award this certificate to
|
||||
<span t-field="user_input.env.company.display_name"/> is proud to award this certificate to
|
||||
<span t-esc="user_input.partner_id.name or user_input.email"/> for graduating <span t-field="user_input.survey_id.display_name"/>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<img src="/survey/static/src/img/certification_seal.svg" alt="Certification Seal" role="img"/>
|
||||
|
||||
<div class="certificate-signature">
|
||||
<span t-field="user_input.survey_id.create_uid.company_id.logo" t-options="{'widget': 'image'}" role="img"/>
|
||||
<span t-field="user_input.env.company.logo" t-options="{'widget': 'image'}" role="img"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,11 @@ const bus = new Bus();
|
||||
* this file someday.
|
||||
*/
|
||||
|
||||
const maxTouchPoints = navigator.maxTouchPoints || 1;
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && maxTouchPoints > 1);
|
||||
const isOtherMobileDevice = /(webOS|BlackBerry|Windows Phone)/i.test(navigator.userAgent);
|
||||
|
||||
var config = {
|
||||
device: {
|
||||
/**
|
||||
@@ -37,6 +42,20 @@ var config = {
|
||||
* @type Number
|
||||
*/
|
||||
size_class: null,
|
||||
/**
|
||||
* Mobile OS (Android) device detection using userAgent.
|
||||
* This flag doesn't depend on the size/resolution of the screen.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
isAndroid: isAndroid,
|
||||
/**
|
||||
* Mobile OS (iOS) device detection using userAgent.
|
||||
* This flag doesn't depend on the size/resolution of the screen.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
isIOS: isIOS,
|
||||
/**
|
||||
* A frequent use case is to have a different render in 'mobile' mode,
|
||||
* meaning when the screen is small. This flag (boolean) is true when
|
||||
@@ -52,13 +71,7 @@ var config = {
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isMobileDevice: navigator.userAgent.match(/Android/i) ||
|
||||
navigator.userAgent.match(/webOS/i) ||
|
||||
navigator.userAgent.match(/iPhone/i) ||
|
||||
navigator.userAgent.match(/iPad/i) ||
|
||||
navigator.userAgent.match(/iPod/i) ||
|
||||
navigator.userAgent.match(/BlackBerry/i) ||
|
||||
navigator.userAgent.match(/Windows Phone/i),
|
||||
isMobileDevice: isAndroid || isIOS || isOtherMobileDevice,
|
||||
/**
|
||||
* Mapping between the numbers 0,1,2,3,4,5,6 and some descriptions
|
||||
*/
|
||||
|
||||
@@ -118,7 +118,11 @@ var SnippetEditor = Widget.extend({
|
||||
}
|
||||
|
||||
var _animationsCount = 0;
|
||||
var postAnimationCover = _.throttle(() => this.cover(), 100);
|
||||
var postAnimationCover = _.throttle(() => {
|
||||
this.trigger_up('cover_update', {
|
||||
overlayVisible: true,
|
||||
});
|
||||
}, 100);
|
||||
this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', () => {
|
||||
// We cannot rely on the fact each transition/animation start will
|
||||
// trigger a transition/animation end as the element may be removed
|
||||
@@ -2327,9 +2331,13 @@ var SnippetsMenu = Widget.extend({
|
||||
* Called when the overlay dimensions/positions should be recomputed.
|
||||
*
|
||||
* @private
|
||||
* @param {FlectraEvent} ev
|
||||
*/
|
||||
_onOverlaysCoverUpdate: function () {
|
||||
_onOverlaysCoverUpdate: function (ev) {
|
||||
this.snippetEditors.forEach(editor => {
|
||||
if (ev.data.overlayVisible) {
|
||||
editor.toggleOverlayVisibility(true);
|
||||
}
|
||||
editor.cover();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ flectra.define('web_editor.snippets.options', function (require) {
|
||||
var core = require('web.core');
|
||||
const {ColorpickerWidget} = require('web.Colorpicker');
|
||||
const Dialog = require('web.Dialog');
|
||||
const {scrollTo} = require('web.dom');
|
||||
const rpc = require('web.rpc');
|
||||
const time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
@@ -4258,6 +4259,18 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
|
||||
left: position[0] / 100 * delta.x || 0,
|
||||
top: position[1] / 100 * delta.y || 0,
|
||||
};
|
||||
// Make sure the element is in a visible area.
|
||||
const rect = this.$target[0].getBoundingClientRect();
|
||||
const viewportTop = $(window).scrollTop();
|
||||
const viewportBottom = viewportTop + $(window).height();
|
||||
const visibleHeight = rect.top < viewportTop
|
||||
? Math.max(0, Math.min(viewportBottom, rect.bottom) - viewportTop) // Starts above
|
||||
: rect.top < viewportBottom
|
||||
? Math.min(viewportBottom, rect.bottom) - rect.top // Starts inside
|
||||
: 0; // Starts after
|
||||
if (visibleHeight < 200) {
|
||||
await scrollTo(this.$target[0], {extraOffset: 50});
|
||||
}
|
||||
this._toggleBgOverlay(true);
|
||||
},
|
||||
/**
|
||||
@@ -4339,7 +4352,7 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
|
||||
height: `${this.$target.innerHeight()}px`,
|
||||
});
|
||||
|
||||
const topPos = (parseInt(this.$overlay.css('top')) - parseInt(this.$overlayContent.css('top')));
|
||||
const topPos = Math.max(0, $(window).scrollTop() - this.$target.offset().top);
|
||||
this.$overlayContent.find('.o_we_overlay_buttons').css('top', `${topPos}px`);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -173,8 +173,18 @@ var LinkDialog = Dialog.extend({
|
||||
this.$styleInputs.prop('checked', false).filter('[value=""]').prop('checked', true);
|
||||
if (this.data.iniClassName) {
|
||||
_.each(this.$('input[name="link_style_color"], select[name="link_style_size"] > option, select[name="link_style_shape"] > option'), el => {
|
||||
var $option = $(el);
|
||||
if ($option.val() && this.data.iniClassName.match(new RegExp('(^|btn-| |btn-outline-)' + $option.val()))) {
|
||||
const $option = $(el);
|
||||
const value = $option.val();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const subValues = value.split(',');
|
||||
let active = true;
|
||||
for (let i = 0; i < subValues.length; i++) {
|
||||
const classPrefix = new RegExp('(^|btn-| |btn-outline-|btn-fill-)' + subValues[i]);
|
||||
active = active && classPrefix.test(this.data.iniClassName);
|
||||
}
|
||||
if (active) {
|
||||
if ($option.is("input")) {
|
||||
$option.prop("checked", true);
|
||||
} else {
|
||||
|
||||
@@ -283,6 +283,86 @@ function removeLoader() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base64 SVG into a base64 PNG.
|
||||
*
|
||||
* @param {string|HTMLImageElement} src - an URL to a SVG or a *loaded* image
|
||||
* with such an URL. This allows the call to this method to be potentially
|
||||
* not return a Promise.
|
||||
* @param {boolean} [noAsync=false] In case, the given first parameter is a
|
||||
* loaded image, this parameter allows to ignore problematic images and
|
||||
* return a (problematic) PNG result synchronously.
|
||||
* @returns {Promise<string>|string} a base64 PNG (as result of a Promise or not)
|
||||
*/
|
||||
function svgToPNG(src, noAsync = false) {
|
||||
function checkImg(imgEl) {
|
||||
// Firefox does not support drawing SVG to canvas unless it has width
|
||||
// and height attributes set on the root <svg>.
|
||||
return (imgEl.naturalHeight !== 0);
|
||||
}
|
||||
function toPNGViaCanvas(imgEl) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imgEl.width;
|
||||
canvas.height = imgEl.height;
|
||||
canvas.getContext('2d').drawImage(imgEl, 0, 0);
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
// In case we receive a loaded image with the given src and that this image
|
||||
// is not problematic, we can convert it to PNG synchronously.
|
||||
if (src instanceof HTMLImageElement) {
|
||||
const loadedImgEl = src;
|
||||
if (noAsync || checkImg(loadedImgEl)) {
|
||||
return toPNGViaCanvas(loadedImgEl);
|
||||
}
|
||||
src = loadedImgEl.src;
|
||||
}
|
||||
|
||||
// At this point, we either did not receive a loaded image or the received
|
||||
// loaded image is problematic => we have to do some asynchronous code and
|
||||
// the function will thus return a Promise.
|
||||
return new Promise(resolve => {
|
||||
const imgEl = new Image();
|
||||
imgEl.onload = () => {
|
||||
if (checkImg(imgEl)) {
|
||||
resolve(imgEl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set arbitrary height on image and attach it to the DOM to force
|
||||
// width computation.
|
||||
imgEl.height = 1000;
|
||||
imgEl.style.opacity = 0;
|
||||
document.body.appendChild(imgEl);
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', imgEl.src, true);
|
||||
request.onload = () => {
|
||||
// Convert the data URI to a SVG element
|
||||
const parser = new DOMParser();
|
||||
const result = parser.parseFromString(request.responseText, 'text/xml');
|
||||
const svgEl = result.getElementsByTagName("svg")[0];
|
||||
|
||||
// Add the attributes Firefox needs and remove the image from
|
||||
// the DOM.
|
||||
svgEl.setAttribute('width', imgEl.width);
|
||||
svgEl.setAttribute('height', imgEl.height);
|
||||
imgEl.remove();
|
||||
|
||||
// Convert the SVG element to a data URI
|
||||
const svg64 = btoa(new XMLSerializer().serializeToString(svgEl));
|
||||
const finalImg = new Image();
|
||||
finalImg.onload = () => {
|
||||
resolve(finalImg);
|
||||
};
|
||||
finalImg.src = `data:image/svg+xml;base64,${svg64}`;
|
||||
};
|
||||
request.send();
|
||||
};
|
||||
imgEl.src = src;
|
||||
}).then(loadedImgEl => toPNGViaCanvas(loadedImgEl));
|
||||
}
|
||||
|
||||
return {
|
||||
loadAnchors: loadAnchors,
|
||||
autocompleteWithPages: autocompleteWithPages,
|
||||
@@ -291,5 +371,6 @@ return {
|
||||
sendRequest: sendRequest,
|
||||
websiteDomain: websiteDomain,
|
||||
removeLoader: removeLoader,
|
||||
svgToPNG: svgToPNG,
|
||||
};
|
||||
});
|
||||
@@ -187,6 +187,23 @@ $-seen-urls: ();
|
||||
}
|
||||
}
|
||||
|
||||
body:not(.o_fullscreen) {
|
||||
#oe_main_menu_navbar:not(.o_hidden) + #wrapwrap {
|
||||
.o_header_affixed, .modal {
|
||||
// For these elements, in #wrapwrap, position top should be size of
|
||||
// the navbar.
|
||||
top: $o-navbar-height;
|
||||
}
|
||||
.modal {
|
||||
// As the default bootstrap style no longer uses 'bottom: 0' and
|
||||
// added 'height: 100%' since the V4.2 because of a bug on Android.
|
||||
// We have to adapt the height of the modal by removing the height
|
||||
// of the navbar.
|
||||
height: calc(100vh - #{$o-navbar-height});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
|
||||
.navbar-collapse {
|
||||
@@ -773,9 +790,6 @@ $-transition-duration: 200ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
#oe_main_menu_navbar + #wrapwrap .o_header_affixed {
|
||||
top: $o-navbar-height;
|
||||
}
|
||||
|
||||
// Navbar
|
||||
.navbar .o_extra_menu_items.show > ul {
|
||||
|
||||
@@ -27,6 +27,10 @@ body.o_connected_user {
|
||||
|
||||
&.editor_enable.editor_has_snippets {
|
||||
padding-right: 0 !important;
|
||||
|
||||
.modal:not(.o_technical_modal) {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
#oe_main_menu_navbar, #web_editor-top-edit {
|
||||
transform: translateY(-100%);
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
</div>
|
||||
<!-- Social -->
|
||||
<div class="col-lg-6 pb16">
|
||||
<div class="s_share text-left no_icon_color" data-name="Social Media">
|
||||
<div class="s_share text-left no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook m-1"/>
|
||||
@@ -490,7 +490,7 @@
|
||||
<div class="oe_structure oe_structure_solo" id="oe_structure_header_vertical_1">
|
||||
<section class="s_text_block" data-snippet="s_text_block" data-name="Text">
|
||||
<div class="container">
|
||||
<div class="s_share no_icon_color" data-name="Social Media">
|
||||
<div class="s_share no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<h6 class="s_share_title d-none">Follow us</h6>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook m-1"/>
|
||||
@@ -592,7 +592,7 @@
|
||||
<section class="s_text_block" data-snippet="s_text_block" data-name="Text">
|
||||
<div class="container">
|
||||
<!-- Social -->
|
||||
<div class="s_share mt-3" data-name="Social Media">
|
||||
<div class="s_share mt-3" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook rounded-circle shadow-sm"/>
|
||||
@@ -778,7 +778,7 @@
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-right">
|
||||
<div class="s_share no_icon_color" data-name="Social Media">
|
||||
<div class="s_share no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<small class="s_share_title text-muted"><b>Follow us</b></small>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook m-1"/>
|
||||
@@ -906,7 +906,7 @@
|
||||
<div class="oe_structure oe_structure_solo" id="oe_structure_header_boxed_1">
|
||||
<section class="s_text_block" data-snippet="s_text_block" name="Text">
|
||||
<div class="container">
|
||||
<div class="s_share text-left" data-name="Social Media">
|
||||
<div class="s_share text-left" data-snippet="s_share" data-name="Social Media">
|
||||
<h6 class="s_share_title d-none">Follow us</h6>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook m-1"/>
|
||||
@@ -1103,10 +1103,10 @@
|
||||
<template id="template_header_hamburger_full_oe_structure_header_hamburger_full_1" inherit_id="website.template_header_hamburger_full" name="Template Header Hamburger Full (oe_structure_header_hamburger_full_1)">
|
||||
<xpath expr="//*[hasclass('oe_structure')][@id='oe_structure_header_hamburger_full_1']" position="replace">
|
||||
<div class="oe_structure oe_structure_solo w-100" id="oe_structure_header_hamburger_full_1">
|
||||
<div class="s_hr pt32 pb32" data-name="Separator">
|
||||
<div class="s_hr pt32 pb32" data-snippet="s_hr" data-name="Separator">
|
||||
<hr class="w-100 mx-auto" style="border-top-width: 1px; border-top-style: solid; border-color: var(--400);"/>
|
||||
</div>
|
||||
<div class="s_share text-center" data-name="Social Media">
|
||||
<div class="s_share text-center" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook rounded-circle shadow-sm"/>
|
||||
@@ -1186,7 +1186,7 @@
|
||||
<p class="mb-0"><small>We help you grow your business</small></p>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-right pt16">
|
||||
<div class="s_share no_icon_color" data-name="Social Media">
|
||||
<div class="s_share no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<small class="s_share_title d-none"><b>Follow us</b></small>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook m-1"/>
|
||||
@@ -1374,7 +1374,7 @@
|
||||
<li><i class="fa fa-envelope fa-fw mr-2"/><span><a href="mailto:info@yourcompany.example.com">info@yourcompany.example.com</a></span></li>
|
||||
<li><i class="fa fa-phone fa-fw mr-2"/><span class="o_force_ltr"><a href="tel:+1 (650) 555-0111">+1 (650) 555-0111</a></span></li>
|
||||
</ul>
|
||||
<div class="s_share text-left" data-name="Social Media">
|
||||
<div class="s_share text-left" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook rounded-circle shadow-sm"/>
|
||||
@@ -1400,7 +1400,7 @@
|
||||
<template id="template_footer_descriptive" inherit_id="website.layout" name="Descriptive" active="False">
|
||||
<xpath expr="//div[@id='footer']" position="replace">
|
||||
<div id="footer" class="oe_structure oe_structure_solo" t-ignore="true" t-if="not no_footer">
|
||||
<section class="s_title pt48 pb24">
|
||||
<section class="s_title pt48 pb24" data-vcss="001" data-snippet="s_title">
|
||||
<div class="container s_allow_columns">
|
||||
<h4><b>Designed</b> for companies</h4>
|
||||
</div>
|
||||
@@ -1419,7 +1419,7 @@
|
||||
<li><i class="fa fa-phone fa-fw mr-2"/><span class="o_force_ltr"><a href="tel:+1 (650) 555-0111">+1 (650) 555-0111</a></span></li>
|
||||
<li><i class="fa fa-envelope fa-fw mr-2"/><span><a href="mailto:hello@mycompany.com">hello@mycompany.com</a></span></li>
|
||||
</ul>
|
||||
<div class="s_share text-left no_icon_color" data-name="Social Media">
|
||||
<div class="s_share text-left no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/github" class="s_share_github" target="_blank">
|
||||
<i class="fa fa-2x fa-github m-1"/>
|
||||
@@ -1447,7 +1447,7 @@
|
||||
<div id="footer" class="oe_structure oe_structure_solo" t-ignore="true" t-if="not no_footer">
|
||||
<section class="s_text_block pt32" data-snippet="s_text_block" data-name="Text">
|
||||
<div class="container s_allow_columns">
|
||||
<div class="s_share text-center mb-4" data-name="Social Media">
|
||||
<div class="s_share text-center mb-4" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-facebook rounded-circle rounded shadow-sm"/>
|
||||
@@ -1552,7 +1552,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-3 pt16 pb16">
|
||||
<div class="s_share text-right no_icon_color" data-name="Social Media">
|
||||
<div class="s_share text-right no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Follow us</h5>
|
||||
<a href="/website/social/github" class="s_share_github" target="_blank">
|
||||
<i class="fa fa-2x fa-github m-1"/>
|
||||
@@ -1594,7 +1594,7 @@
|
||||
<h5><a href="mailto:hello@mycompany.com">hello@mycompany.com</a></h5>
|
||||
</div>
|
||||
<div class="col-lg-3 pt16 pb16">
|
||||
<div class="s_share text-right no_icon_color" data-name="Social Media">
|
||||
<div class="s_share text-right no_icon_color" data-snippet="s_share" data-name="Social Media">
|
||||
<p class="s_share_title d-block mb-2">Follow us</p>
|
||||
<a href="/website/social/twitter" class="s_share_twitter" target="_blank">
|
||||
<i class="fa fa-twitter m-1"/>
|
||||
@@ -1715,7 +1715,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-3 pb24">
|
||||
<div class="s_share text-right" data-name="Social Media">
|
||||
<div class="s_share text-right" data-snippet="s_share" data-name="Social Media">
|
||||
<h5 class="s_share_title d-none">Share</h5>
|
||||
<a href="/website/social/facebook" class="s_share_facebook" target="_blank">
|
||||
<i class="fa fa-1x fa-facebook rounded-circle shadow-sm"/>
|
||||
|
||||
@@ -33,7 +33,7 @@ class Lead(models.Model):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("website.website_visitor_page_action")
|
||||
action['domain'] = [('visitor_id', 'in', visitors.ids)]
|
||||
# avoid grouping if only few records
|
||||
if len(visitors.website_track_ids.ids) > 15 and len(visitors.page_ids.ids) > 1:
|
||||
if len(visitors.website_track_ids) > 15 and len(visitors.website_track_ids.page_id) > 1:
|
||||
action['context'] = {'search_default_group_by_page': '1'}
|
||||
return action
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ const FormEditor = options.Class.extend({
|
||||
}
|
||||
return field.records;
|
||||
},
|
||||
/**
|
||||
* Generates a new ID.
|
||||
*
|
||||
* @private
|
||||
* @returns {string} The new ID
|
||||
*/
|
||||
_generateUniqueID() {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
},
|
||||
/**
|
||||
* Returns a field object
|
||||
*
|
||||
@@ -119,10 +128,10 @@ const FormEditor = options.Class.extend({
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} field
|
||||
* @returns {Promise<HTMLElement>}
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
_renderField: function (field) {
|
||||
field.id = Math.random().toString(36).substring(2, 15); // Big unique ID
|
||||
field.id = this._generateUniqueID();
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = qweb.render("website_form.field_" + field.type, {field: field}).trim();
|
||||
return template.content.firstElementChild;
|
||||
@@ -426,10 +435,10 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
|
||||
field.formatInfo.requiredMark = this._isRequiredMark();
|
||||
field.formatInfo.optionalMark = this._isOptionalMark();
|
||||
field.formatInfo.mark = this._getMark();
|
||||
const htmlField = this._renderField(field);
|
||||
data.$target.after(htmlField);
|
||||
const fieldEl = this._renderField(field);
|
||||
data.$target.after(fieldEl);
|
||||
this.trigger_up('activate_snippet', {
|
||||
$snippet: $(htmlField),
|
||||
$snippet: $(fieldEl),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -821,6 +830,19 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
// We need to reload the existing type list.
|
||||
this.rerender = true;
|
||||
},
|
||||
/**
|
||||
* Rerenders the clone to avoid id duplicates.
|
||||
* Rendering the list before replacing the field is needed
|
||||
* for selects, in order to build this.listTable.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onClone() {
|
||||
this._renderList();
|
||||
const field = this._getActiveField();
|
||||
const fieldEl = this._renderField(field);
|
||||
this._replaceFieldElement(fieldEl);
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Options
|
||||
@@ -978,7 +1000,7 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Replace the target content with the field provided
|
||||
* Replaces the target content with the field provided.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} field
|
||||
@@ -986,11 +1008,20 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
*/
|
||||
_replaceField: async function (field) {
|
||||
await this._fetchFieldRecords(field);
|
||||
const htmlField = this._renderField(field);
|
||||
const fieldEl = this._renderField(field);
|
||||
this._replaceFieldElement(fieldEl);
|
||||
},
|
||||
/**
|
||||
* Replaces the target with provided field.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} fieldEl
|
||||
*/
|
||||
_replaceFieldElement(fieldEl) {
|
||||
[...this.$target[0].childNodes].forEach(node => node.remove());
|
||||
[...htmlField.childNodes].forEach(node => this.$target[0].appendChild(node));
|
||||
[...htmlField.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));
|
||||
[...htmlField.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));
|
||||
[...fieldEl.childNodes].forEach(node => this.$target[0].appendChild(node));
|
||||
[...fieldEl.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));
|
||||
[...fieldEl.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1153,7 +1184,7 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
const params = {
|
||||
field: {
|
||||
name: name,
|
||||
id: Math.random().toString(36).substring(2, 15), // Big unique ID
|
||||
id: this._generateUniqueID(),
|
||||
required: isRequiredField,
|
||||
formatInfo: {
|
||||
multiPosition: multiInputsWrap.dataset.display,
|
||||
@@ -1270,10 +1301,10 @@ options.registry.AddFieldForm = FormEditor.extend({
|
||||
addField: async function (previewMode, value, params) {
|
||||
const field = this._getCustomField('char', 'Custom Text');
|
||||
field.formatInfo = this._getDefaultFormat();
|
||||
const htmlField = this._renderField(field);
|
||||
this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(htmlField);
|
||||
const fieldEl = this._renderField(field);
|
||||
this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(fieldEl);
|
||||
this.trigger_up('activate_snippet', {
|
||||
$snippet: $(htmlField),
|
||||
$snippet: $(fieldEl),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var utils = require('web.utils');
|
||||
var wUtils = require('website.utils');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -152,20 +153,38 @@ var SlideUploadDialog = Dialog.extend({
|
||||
'datas': this.file.data
|
||||
});
|
||||
} else if (values['slide_type'] === 'webpage') {
|
||||
let fileData = this.file.type === 'image/svg+xml' ? this.__svgToPNG() : this.file.data;
|
||||
if (fileData instanceof Promise) {
|
||||
const fileDataProm = fileData;
|
||||
this.__svgLoadingExec = () => fileDataProm.then(result => values['image_1920'] = result);
|
||||
fileData = this._svgToPng();
|
||||
}
|
||||
_.extend(values, {
|
||||
'mime_type': 'text/html',
|
||||
'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data,
|
||||
'image_1920': fileData,
|
||||
});
|
||||
} else if (/^image\/.*/.test(this.file.type)) {
|
||||
let fileData = this.file.type === 'image/svg+xml' ? this.__svgToPNG() : this.file.data;
|
||||
let fileDataProm;
|
||||
if (fileData instanceof Promise) {
|
||||
fileDataProm = fileData;
|
||||
fileData = this._svgToPng();
|
||||
}
|
||||
if (values['slide_type'] === 'presentation') {
|
||||
if (fileDataProm) {
|
||||
this.__svgLoadingExec = () => fileDataProm.then(result => values['datas'] = result);
|
||||
}
|
||||
_.extend(values, {
|
||||
'slide_type': 'infographic',
|
||||
'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type,
|
||||
'datas': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data
|
||||
'datas': fileData,
|
||||
});
|
||||
} else {
|
||||
if (fileDataProm) {
|
||||
this.__svgLoadingExec = () => fileDataProm.then(result => values['image_1920'] = result);
|
||||
}
|
||||
_.extend(values, {
|
||||
'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data,
|
||||
'image_1920': fileData,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -373,13 +392,22 @@ var SlideUploadDialog = Dialog.extend({
|
||||
// TODO: Remove this part, as now SVG support in image resize tools is included
|
||||
//Python PIL does not support SVG, so converting SVG to PNG
|
||||
_svgToPng: function () {
|
||||
var img = this.$el.find('img#slide-image')[0];
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||
return canvas.toDataURL('image/png').split(',')[1];
|
||||
return this.__svgToPNG(true);
|
||||
},
|
||||
/**
|
||||
* Bug fixed version of the original _svgToPng, it can return a Promise
|
||||
*
|
||||
* @returns {Promise<string>|string}
|
||||
*/
|
||||
__svgToPNG: function (noAsync = false) {
|
||||
const imgEl = this.$el.find('img#slide-image')[0];
|
||||
const result = wUtils.svgToPNG(imgEl, noAsync);
|
||||
if (typeof(result) === 'string') {
|
||||
return result.split(',')[1];
|
||||
}
|
||||
return result.then(png => png.split(',')[1]);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -601,9 +629,18 @@ var SlideUploadDialog = Dialog.extend({
|
||||
var values = this._formValidateGetValues($btn.hasClass('o_w_slide_upload_published')); // get info before changing state
|
||||
var oldType = this.get('state');
|
||||
this.set('state', '_upload');
|
||||
return this._rpc({
|
||||
route: '/slides/add_slide',
|
||||
params: values,
|
||||
|
||||
return new Promise(async resolve => {
|
||||
if (this.__svgLoadingExec) {
|
||||
await this.__svgLoadingExec();
|
||||
}
|
||||
delete this.__svgLoadingExec;
|
||||
resolve();
|
||||
}).then(() => {
|
||||
return this._rpc({
|
||||
route: '/slides/add_slide',
|
||||
params: values,
|
||||
});
|
||||
}).then(function (data) {
|
||||
self._onFormSubmitDone(data, oldType);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user