[PATCH] Upstream patch

This commit is contained in:
Parthiv Patel committed 2021-06-27 10:01:14 +00:00
1 parent 45ece082e2
commit 93293adb19
90 files changed
+1081 -233

No files matched your search

@@ -780,6 +780,7 @@ class AccountBankStatementLine(models.Model):
# -------------------------------------------------------------------------
@api.depends('journal_id', 'currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
'move_id.to_check',
'move_id.line_ids.account_id', 'move_id.line_ids.amount_currency',
'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.currency_id',
'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids')
+2 -2
View File
@@ -752,8 +752,8 @@ class AccountPayment(models.Model):
for line in writeoff_lines:
line_ids_commands.append((2, line.id))
if writeoff_lines:
line_ids_commands.append((0, 0, line_vals_list[2]))
for extra_line_vals in line_vals_list[2:]:
line_ids_commands.append((0, 0, extra_line_vals))
# Update the existing journal items.
# If dealing with multiple write-off lines, they are dropped and a new one is generated.
@@ -19,6 +19,7 @@ tour.register('account_tour', {
trigger: "button[name=action_save_onboarding_company_step]",
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
content: _t("Looks good. Let's continue."),
position: "left",
}, {
trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
content: _t("Customize your layout."),
@@ -27,6 +28,7 @@ tour.register('account_tour', {
trigger: "button[name=document_layout_save]",
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
content: _t("Once everything is as you want it, validate."),
position: "left",
}, {
trigger: "a.o_onboarding_step_action[data-method=action_open_account_onboarding_create_invoice]",
content: _t("Now, we'll create your first invoice."),
@@ -84,6 +86,7 @@ tour.register('account_tour', {
trigger: "button[name=send_and_print_action]",
extra_trigger: "[name=move_type][raw-value=out_invoice]",
content: _t("Let's send the invoice."),
position: "left"
}
]);
@@ -269,23 +269,6 @@ class AccountPaymentRegister(models.TransientModel):
else:
wizard.partner_bank_id = False
@api.depends('journal_id')
def _compute_payment_method_id(self):
for wizard in self:
batches = wizard._get_batches()
payment_type = batches[0]['key_values']['payment_type']
if payment_type == 'inbound':
available_payment_methods = wizard.journal_id.inbound_payment_method_ids
else:
available_payment_methods = wizard.journal_id.outbound_payment_method_ids
# Select the first available one by default.
if available_payment_methods:
wizard.payment_method_id = available_payment_methods[0]._origin
else:
wizard.payment_method_id = False
@api.depends('payment_type',
'journal_id.inbound_payment_method_ids',
'journal_id.outbound_payment_method_ids')
@@ -8,6 +8,17 @@ from flectra.tools.misc import formatLang, format_date
INV_LINES_PER_STUB = 9
class AccountPaymentRegister(models.TransientModel):
_inherit = "account.payment.register"
@api.depends('payment_type', 'journal_id', 'partner_id')
def _compute_payment_method_id(self):
super()._compute_payment_method_id()
for record in self:
preferred = record.partner_id.with_company(record.company_id).property_payment_method_id
if record.payment_type == 'outbound' and preferred in record.journal_id.outbound_payment_method_ids:
record.payment_method_id = preferred
class AccountPayment(models.Model):
_inherit = "account.payment"
@@ -83,7 +83,7 @@
<h3>Contractual Relationship</h3>
<p>The payment processing services ordered by you by placing this order will be
provided to you by Adyen N.V. (hereafter “Processor”), with which you are
entering into a direct agreement by confirming this order. FlectraHQ, Odoo S.A.
entering into a direct agreement by confirming this order. FlectraHQ, Inc., Odoo S.A.
(hereafter “We /Us”) will assist and support you in your use of the services to be
provided by the Processor and we will provide you first line assistance with and
enable you to connect to the systems of Processor to be able to use its services.
@@ -122,6 +122,7 @@ class CompanyLDAP(models.Model):
return False
except ldap.LDAPError as e:
_logger.error('An LDAP exception occurred: %s', e)
return False
return entry
def _query(self, conf, filter, retrieve_attributes=None):
+1 -1
View File
@@ -8,7 +8,7 @@
Allow users to login through OAuth2 Provider.
=============================================
""",
'maintainer': 'FlectraHQ, Odoo S.A.',
'maintainer': 'FlectraHQ Inc.., Odoo S.A.',
'depends': ['base', 'web', 'base_setup', 'auth_signup'],
'data': [
'data/auth_oauth_data.xml',
@@ -213,7 +213,7 @@
</span>
</t>
<t t-name="ImportView.error.multi.body">
<span class="oe_import_report_message">
<span class="oe_import_report_message" t-if="error.rows">
<t t-esc="at_multi(error.rows)"/>
</span>
</t>
+1 -1
View File
@@ -23,7 +23,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
'stop': '2011-04-30 18:30:00',
'description': 'The Technical Presentation will cover following topics:\n* Creating Flectra class\n* Views\n* Wizards\n* Workflows',
'duration': 2.5,
'location': 'FlectraHQ, Odoo S.A.',
'location': 'FlectraHQ, Inc., Odoo S.A.',
'name': 'Technical Presentation'
})
+1 -1
View File
@@ -138,7 +138,7 @@ class Team(models.Model):
as a member of one of the Sales Team.
</p>""")
if user_team_id:
action['help'] += "<p>As you don't belong to any Sales Team, Flectra opens the first one by default.</p>"
action['help'] += _("<p>As you don't belong to any Sales Team, Flectra opens the first one by default.</p>")
action_context = safe_eval(action['context'], {'uid': self.env.uid})
if user_team_id:
+1 -1
View File
@@ -36,7 +36,7 @@ class StockMoveLine(models.Model):
for move_line in self:
if move_line.move_id.sale_line_id:
unit_price = move_line.move_id.sale_line_id.price_reduce_taxinc
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.move_id.sale_line_id.product_uom)
qty = move_line.product_uom_id._compute_quantity(move_line.move_id.sale_line_id.product_qty, move_line.move_id.sale_line_id.product_uom)
else:
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)
+1 -1
View File
@@ -259,7 +259,7 @@
<record id="employee_resume_line_admin_4" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">FlectraHQ</field>
<field name="name">FlectraHQ, Inc.</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-3)).strftime('%Y-11-01')"/>
<field name="line_type_id" ref="resume_type_experience"/>
<field name="description">
+10 -8
View File
@@ -15,13 +15,15 @@
name="_init_data_analytic_account"
eval="[]"/>
<record id="ir_config_parameter_timesheet_rounding" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_rounding</field>
<field name="value">15</field>
</record>
<record id="ir_config_parameter_timesheet_min_duration" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_min_duration</field>
<field name="value">15</field>
</record>
<data noupdate="1">
<record id="ir_config_parameter_timesheet_rounding" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_rounding</field>
<field name="value">15</field>
</record>
<record id="ir_config_parameter_timesheet_min_duration" model="ir.config_parameter">
<field name="key">hr_timesheet.timesheet_min_duration</field>
<field name="value">15</field>
</record>
</data>
</flectra>
+1 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
{
'name': 'Argentina - Accounting',
'version': "3.3",
'version': "3.4",
'description': """
Functional
----------
+4 -3
View File
@@ -55,10 +55,10 @@ class AccountJournal(models.Model):
'3': ['B', 'C', 'I'],
'4': ['B', 'C', 'I'],
'5': ['B', 'C', 'I'],
'6': ['B', 'C', 'I'],
'6': ['A', 'B', 'C', 'I'],
'9': ['E'],
'10': ['E'],
'13': ['B', 'C', 'I'],
'13': ['A', 'B', 'C', 'I'],
'99': ['B', 'C', 'I']
},
}
@@ -83,6 +83,7 @@ class AccountJournal(models.Model):
receipt_m_code = ['54']
receipt_codes = ['4', '9', '15']
expo_codes = ['19', '20', '21']
liq_product_codes = ['60', '61']
if self.type != 'sale':
return []
elif self.l10n_ar_afip_pos_system == 'II_IM':
@@ -90,7 +91,7 @@ class AccountJournal(models.Model):
return usual_codes + receipt_codes + expo_codes + invoice_m_code + receipt_m_code
elif self.l10n_ar_afip_pos_system in ['RAW_MAW', 'RLI_RLM']:
# electronic/online invoice
return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes
return usual_codes + receipt_codes + invoice_m_code + receipt_m_code + mipyme_codes + liq_product_codes
elif self.l10n_ar_afip_pos_system in ['CPERCEL', 'CPEWS']:
# invoice with detail
return usual_codes + invoice_m_code
+8
View File
@@ -239,6 +239,14 @@
</div>
</xpath>
<!-- RG 5003: Add legend for 'A' documents that have a Monotribuista receptor -->
<p name="comment" position="after">
<p t-if="o.partner_id.l10n_ar_afip_responsibility_type_id.code in ['6', '13'] and o.l10n_latam_document_type_id.l10n_ar_letter == 'A'" >
El crédito fiscal discriminado en el presente comprobante, sólo podrá ser computado a efectos del Régimen de Sostenimiento e Inclusión Fiscal para Pequeños Contribuyentes de la Ley Nº 27.618
</p>
</p>
</template>
<!-- FIXME: Temp fix to allow fetching invoice_documemt in Studio Reports with localisation -->
+1 -1
View File
@@ -34,7 +34,7 @@ Wizards provided by this module:
**Path to access :** Invoicing/Reporting/Legal Reports/Belgium Statements/Annual Listing Of VAT-Subjected Customers
""",
'author': 'Noviat, FlectraHQ',
'author': 'Noviat, FlectraHQ Inc..',
'depends': [
'account',
'base_iban',
+1 -1
View File
@@ -11,7 +11,7 @@ However, if the new QR-IBAN field is filled, the value will be used as the QR-IB
This should help for reconciliation on the bank statements where the old IBAN code is still used.
""",
'version': '1.0',
'author': 'FlectraHQ, Odoo S.A',
'author': 'FlectraHQ Inc.., Odoo S.A',
'category': 'Localization',
'depends': ['l10n_ch'],
'data': [
+4 -1
View File
@@ -2,4 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import datev
from . import chart_template
from . import base_document_layout
from . import chart_template
from . import ir_actions_report
from . import account_move
+40
View File
@@ -0,0 +1,40 @@
from flectra import models, fields, _
from flectra.tools import format_date
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_de_template_data = fields.Binary(compute='_compute_l10n_de_template_data')
l10n_de_document_title = fields.Char(compute='_compute_l10n_de_document_title')
def _compute_l10n_de_template_data(self):
for record in self:
record.l10n_de_template_data = data = []
if record.name:
data.append((_("Invoice No."), record.name))
if record.invoice_date:
data.append((_("Invoice Date"), format_date(self.env, record.invoice_date)))
if record.invoice_date_due:
data.append((_("Due Date"), format_date(self.env, record.invoice_date_due)))
if record.invoice_origin:
data.append((_("Source"), record.invoice_origin))
if record.ref:
data.append((_("Reference"), record.ref))
def _compute_l10n_de_document_title(self):
for record in self:
record.l10n_de_document_title = ''
if record.move_type == 'out_invoice':
if record.state == 'posted':
record.l10n_de_document_title = _('Invoice')
elif record.state == 'draft':
record.l10n_de_document_title = _('Draft Invoice')
elif record.state == 'cancel':
record.l10n_de_document_title = _('Cancelled Invoice')
elif record.move_type == 'out_refund':
record.l10n_de_document_title = _('Credit Note')
elif record.move_type == 'in_refund':
record.l10n_de_document_title = _('Vendor Credit Note')
elif record.move_type == 'in_invoice':
record.l10n_de_document_title = _('Vendor Bill')
@@ -0,0 +1,26 @@
from flectra import models, fields, _
from flectra.tools import format_date
class BaseDocumentLayout(models.TransientModel):
_inherit = 'base.document.layout'
street = fields.Char(related='company_id.street', readonly=True)
street2 = fields.Char(related='company_id.street2', readonly=True)
zip = fields.Char(related='company_id.zip', readonly=True)
city = fields.Char(related='company_id.city', readonly=True)
company_registry = fields.Char(related='company_id.company_registry', readonly=True)
bank_ids = fields.One2many(related='company_id.bank_ids', readonly=True)
l10n_de_template_data = fields.Binary(compute='_compute_l10n_de_template_data')
l10n_de_document_title = fields.Char(compute='_compute_l10n_de_document_title')
def _compute_l10n_de_template_data(self):
self.l10n_de_template_data = [
(_("Invoice No."), 'INV/2021/12345'),
(_("Invoice Date"), format_date(self.env, fields.Date.today())),
(_("Due Date"), format_date(self.env, fields.Date.add(fields.Date.today(), days=7))),
(_("Reference"), 'SO/2021/45678'),
]
def _compute_l10n_de_document_title(self):
self.l10n_de_document_title = 'Invoice'
@@ -0,0 +1,10 @@
from flectra import models
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
def _get_rendering_context(self, docids, data):
data = super()._get_rendering_context(docids, data)
data['din_header_spacing'] = self.get_paperformat().header_spacing
return data
+158 -75
View File
@@ -3,15 +3,30 @@
<data>
<!-- New report paperformat for din5008 format -->
<record id="paperformat_euro_din_a" model="report.paperformat">
<field name="name">European A4 for DIN5008 Type A</field>
<field name="default" eval="False" />
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">27</field>
<field name="margin_bottom">40</field>
<field name="margin_left">20</field>
<field name="margin_right">10</field>
<field name="dpi">70</field>
<field name="header_line" eval="False" />
<field name="header_spacing">27</field>
</record>
<record id="paperformat_euro_din" model="report.paperformat">
<field name="name">European A4 for DIN</field>
<field name="name">European A4 for DIN5008 Type B</field>
<field name="default" eval="False" />
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">45</field>
<field name="margin_bottom">28</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="margin_bottom">40</field>
<field name="margin_left">20</field>
<field name="margin_right">10</field>
<field name="dpi">70</field>
<field name="header_line" eval="False" />
<field name="header_spacing">45</field>
</record>
@@ -24,93 +39,161 @@
<!-- New report layout for din5008 format -->
<template id="external_layout_din5008">
<t t-if="not o and doc">
<t t-set="o" t-value="doc"/>
</t>
<t t-if="o and 'company_id' in o">
<t t-set="company" t-value="o.company_id.sudo()"/>
</t>
<t t-if="not o or not 'company_id' in o">
<t t-set="company" t-value="res_company"/>
</t>
<div>
<div class="header din">
<div class="float-right">
<h3 class="mt0 text-right" t-field="company.report_header"/>
</div>
<img t-if="company.logo" t-att-src="image_data_uri(company.logo)" class="float-left"/>
<div t-attf-class="header din_page o_company_#{company.id}_layout">
<table class="company_header" t-att-style="'height: %dmm;' % (din_header_spacing or 27)">
<tr>
<td><h3 class="mt0" t-field="company.report_header"/></td>
<td><img t-if="company.logo" t-att-src="image_data_uri(company.logo)" t-att-style="'max-height: %dmm;' % (din_header_spacing or 27)"/></td>
</tr>
</table>
</div>
<div class="article din" t-att-data-oe-model="o and o._name" t-att-data-oe-id="o and o.id">
<div class="header_address">
<t t-if="address">
<div class="company_invoice_address">
<t t-if="company.name">
<span t-field="company.name"/> &#8226;
</t>
<t t-if="company.street">
<span t-field="company.street"/> &#8226;
</t>
<t t-if="company.street2">
<span t-field="company.street2"/> &#8226;
</t>
<t t-if="company.zip">
<span t-field="company.zip"/> &#8226;
</t>
<t t-if="company.city">
<span t-field="company.city"/> &#8226;
</t>
<t t-if="company.country_id">
<span t-field="company.country_id.name"/>
</t>
<hr class="company_invoice_line" />
</div>
<div name="invoice_address" class="invoice_address">
<t t-raw="address"/>
</div>
</t>
<t t-else="fallback_address">
<div name="invoice_address" class="invoice_address" t-raw="fallback_address" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}' />
</t>
<div class="company_address">
<div>
<strong t-field="company.partner_id.name"/>
</div>
<span t-field="company.partner_id"
t-options='{"widget": "contact", "fields": ["address"], "no_marker": true}'/>
<t t-if="information_block">
<div t-attf-class="din_page invoice_note article o_company_#{company.id}_layout" t-att-data-oe-model="o and o._name" t-att-data-oe-id="o and o.id">
<table>
<tr>
<td>
<div class="address">
<t t-raw="information_block"/>
<t t-if="company.name">
<span t-field="company.name"/>
</t>
<t t-if="company.street">
<span>|</span> <span t-field="company.street"/>
</t>
<t t-if="company.street2">
<span>|</span> <span t-field="company.street2"/>
</t>
<t t-if="company.zip">
<span>|</span> <span t-field="company.zip"/>
</t>
<t t-if="company.city">
<span t-if="not company.zip">|</span> <span t-field="company.city"/>
</t>
<t t-if="company.country_id">
<span>|</span> <span t-field="company.country_id.name"/>
</t>
<hr class="company_invoice_line" />
<div t-if="address">
<t t-raw="address"/>
</div>
<div t-else="fallback_address">
<t t-esc="fallback_address"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}' />
</div>
</div>
</td>
<td>
<div class="information_block">
<t t-if="'l10n_de_template_data' in company" t-set="template_data" t-value="company.l10n_de_template_data"/>
<t t-if="o and 'l10n_de_template_data' in o" t-set="template_data" t-value="o.l10n_de_template_data"/>
<table>
<t t-foreach="template_data" t-as="row">
<tr><td><t t-esc="row[0]"/></td><td><t t-esc="row[1]"/></td></tr>
</t>
</table>
</div>
</td>
</tr>
<tr t-if="o and 'l10n_de_addresses' in o">
<t t-foreach="o.l10n_de_addresses" t-as="doc_address">
<td>
<div class="shipping_address">
<strong><t t-esc="doc_address[0]"/></strong>
<div t-esc="doc_address[1]" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/>
</div>
</td>
</t>
</div>
</div>
<t t-raw="0" />
</tr>
</table>
<h2>
<span t-if="not o"><t t-esc="company.l10n_de_document_title"/></span>
<span t-elif="'l10n_de_document_title' in o"><t t-esc="o.l10n_de_document_title"/></span>
<span t-else="" t-field="o.name"/>
</h2>
<t t-raw="0"/>
</div>
<div class="footer o_background_footer din">
<div t-attf-class="din_page footer o_company_#{company.id}_layout">
<div class="text-right page_number">
<div class="text-muted">
Page:
<span class="page"/>
of
<span class="topage"/>
Page: <span class="page"/> of <span class="topage"/>
</div>
</div>
<div class="text-center">
<ul class="list-inline">
<li t-if="company.phone"><i class="fa fa-phone"/> <span t-field="company.phone"/></li>
<li t-if="company.email"><i class="fa fa-at"/> <span t-field="company.email"/></li>
<li t-if="company.website"><i class="fa fa-globe"/> <span t-field="company.website"/></li>
<li t-if="company.vat"><i class="fa fa-building-o"/><t t-esc="company.country_id.vat_label or 'Tax ID'"/>: <span t-field="company.vat"/></li>
</ul>
<div t-field="company.report_footer"/>
<div class="company_details">
<table>
<tr>
<td>
<ul class="list-inline">
<li t-if="company.name"><span t-field="company.name"/></li>
<li t-if="company.street"><span t-field="company.street"/></li>
<li t-if="company.street2"><span t-field="company.street2"/></li>
<li><span t-if="company.zip" t-field="company.zip"/> <span t-if="company.city" t-field="company.city"/></li>
<li t-if="company.country_id"><span t-field="company.country_id.name"/></li>
</ul>
</td>
<td>
<ul class="list-inline">
<li t-if="company.phone"><span t-field="company.phone"/></li>
<li t-if="company.email"><span t-field="company.email"/></li>
<li t-if="company.website"><span t-field="company.website"/></li>
</ul>
</td>
<td>
<ul class="list-inline">
<li t-if="company.vat"><t t-esc="company.country_id.vat_label or 'Tax ID'"/>: <span t-field="company.vat"/></li>
<li>HRB Nr: <span t-field="company.company_registry"/></li>
<div t-field="company.report_footer"/>
</ul>
</td>
<td>
<ul class="list-inline" t-if="company.bank_ids">
<t t-foreach="company.bank_ids[:2]" t-as="bank">
<li><span t-field="bank.bank_id.name"/></li>
<li>IBAN: <span t-field="bank.acc_number"/></li>
<li>BIC: <span t-field="bank.bank_id.bic"/></li>
</t>
</ul>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<template id="din5008_css" inherit_id="web.styles_company_report">
<xpath expr="//t[@t-elif]" position="before">
<t t-elif="layout == 'l10n_de.external_layout_din5008'">
&amp;.din_page {
&amp;.header {
.company_header {
.name_container {
color: <t t-esc='primary'/>;
}
}
}
&amp;.invoice_note {
td {
.address {
> span {
color: <t t-esc='secondary'/>;
}
}
}
h2 {
color: <t t-esc='primary'/>;
}
.page {
[name=invoice_line_table], [name=stock_move_table], .o_main_table {
th {
color: <t t-esc='secondary'/>;
}
}
}
}
}
</t>
</xpath>
</template>
</data>
</flectra>
@@ -1,3 +1,110 @@
.din_page {
width: 180mm;
margin-left: -1rem;
font-size: 9pt;
&.header {
table {
width: 100%;
img, h3, td, tr {
padding: 0;
margin: 0;
}
h3 {
color: $o-default-report-primary-color;
position: relative;
top: -10mm;
margin-bottom: -10mm;
}
img {
float: right;
}
}
}
&.invoice_note {
tr {
td {
vertical-align: bottom;
}
&:nth-child(2) td {
vertical-align: top;
}
.address, .information_block, .shipping_address, .invoice_address {
margin: 0;
}
.address {
height: 45mm;
}
.address, .shipping_address {
width: 85mm;
padding-left: 5mm;
.company_invoice_line {
margin-top: 1mm;
}
> span {
color: $o-default-report-secondary-color;
}
}
.information_block, .invoice_address {
width: 75mm;
min-height: 40mm;
margin-left: 20mm;
table {
width: 100%;
}
}
}
h2 {
margin-left: 5mm;
margin-right: 10mm;
margin-top: 8.46mm;
color: $o-default-report-primary-color;
}
> .pt-5 { // hide hardcoded address from base.template.layout
display: none;
}
.page {
margin-left: 5mm;
margin-right: 10mm;
> h2, h1, #informations {
display: none;
}
[name=invoice_line_table], [name=stock_move_table], .o_main_table {
margin-top: 8.46mm;
th {
color: $o-default-report-secondary-color;
}
}
}
}
&.footer {
padding-left: 5mm;
padding-right: 10mm;
.page_number {
margin-top: 4.23mm;
width: 100%;
height: 4.23mm;
}
.company_details {
margin-top: 4.23mm;
width: 100%;
table {
border-top: solid 1px;
width: 100%;
td {
vertical-align: baseline;
padding-right: 3mm;
&:last-child {
padding-right: initial;
}
}
}
}
}
}
// TODO WAN remove in master
.din {
&.header {
min-height: 45mm;
@@ -40,7 +147,7 @@
word-break: break-word;
float: left;
}
.header_address {
.header_address {
min-height: 45mm;
}
.page_number {
+1 -1
View File
@@ -19,7 +19,7 @@ The module adds following features:
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ (only for Flectra Enterprise users)
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ Inc.. (only for Flectra Enterprise users)
""",
'depends': ['l10n_fr', 'point_of_sale'],
'installable': True,
+1 -1
View File
@@ -6,7 +6,7 @@
'version': '1.0',
'category': 'Accounting/Localizations/Account Charts',
'description': """ This is the base module to manage chart of accounting and localization for Hong Kong """,
'author': 'FlectraHQ',
'author': 'FlectraHQ Inc..',
'depends': ['account'],
'data': [
'data/account_chart_template_data.xml',
@@ -29,17 +29,20 @@ class AccountMoveLine(models.Model):
included_taxes = \
invoice.l10n_latam_document_type_id and invoice.l10n_latam_document_type_id._filter_taxes_included(
line.tax_ids)
# For the unit price, we need the number rounded based on the product price precision.
# The method compute_all uses the accuracy of the currency so, we multiply and divide for 10^(decimal accuracy of product price) to get the price correctly rounded.
price_digits = 10**self.env['decimal.precision'].precision_get('Product Price')
if not included_taxes:
price_unit = line.tax_ids.with_context(round=False, force_sign=invoice._get_tax_force_sign()).compute_all(
line.price_unit, invoice.currency_id, 1.0, line.product_id, invoice.partner_id)
l10n_latam_price_unit = price_unit['total_excluded']
line.price_unit * price_digits, invoice.currency_id, 1.0, line.product_id, invoice.partner_id)
l10n_latam_price_unit = price_unit['total_excluded'] / price_digits
l10n_latam_price_subtotal = line.price_subtotal
not_included_taxes = line.tax_ids
l10n_latam_price_net = l10n_latam_price_unit * (1 - (line.discount or 0.0) / 100.0)
else:
not_included_taxes = line.tax_ids - included_taxes
l10n_latam_price_unit = included_taxes.with_context(force_sign=invoice._get_tax_force_sign()).compute_all(
line.price_unit, invoice.currency_id, 1.0, line.product_id, invoice.partner_id)['total_included']
line.price_unit * price_digits, invoice.currency_id, 1.0, line.product_id, invoice.partner_id)['total_included'] / price_digits
l10n_latam_price_net = l10n_latam_price_unit * (1 - (line.discount or 0.0) / 100.0)
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
l10n_latam_price_subtotal = included_taxes.with_context(force_sign=invoice._get_tax_force_sign()).compute_all(
+1 -1
View File
@@ -5,7 +5,7 @@
"name" : "Mongolia - Accounting",
"version" : "1.0",
'category': 'Accounting/Localizations/Account Charts',
"author" : "BumanIT LLC, FlectraHQ, Odoo S.A.",
"author" : "BumanIT LLC, FlectraHQ Inc.., Odoo S.A.",
"description": """
This is the module to manage the accounting chart for Mongolia.
===============================================================
+1 -1
View File
@@ -8,7 +8,7 @@
{
'name' : 'Venezuela - Accounting',
'author': ['FlectraHQ, Odoo S.A.', 'Vauxoo'],
'author': ['FlectraHQ Inc.., Odoo S.A.', 'Vauxoo'],
'category': 'Accounting/Localizations/Account Charts',
'description':
"""
+4 -1
View File
@@ -311,9 +311,12 @@ class MailMail(models.Model):
headers = {}
ICP = self.env['ir.config_parameter'].sudo()
bounce_alias = ICP.get_param("mail.bounce.alias")
bounce_alias_static = tools.str2bool(ICP.get_param("mail.bounce.alias.static", "False"))
catchall_domain = ICP.get_param("mail.catchall.domain")
if bounce_alias and catchall_domain:
if mail.mail_message_id.is_thread_message():
if bounce_alias_static:
headers['Return-Path'] = '%s@%s' % (bounce_alias, catchall_domain)
elif mail.mail_message_id.is_thread_message():
headers['Return-Path'] = '%s+%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
else:
headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain)
+2 -1
View File
@@ -13,6 +13,7 @@ from flectra.exceptions import AccessError, UserError
from flectra.http import request
from flectra.osv import expression
from flectra.tools import groupby
from flectra.tools.misc import clean_context
_logger = logging.getLogger(__name__)
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])(?: data-filename="([^"]*)")?', re.I)
@@ -602,7 +603,7 @@ class Message(models.Model):
values['attachment_ids'] = []
# extract base64 images
if 'body' in values:
Attachments = self.env['ir.attachment']
Attachments = self.env['ir.attachment'].with_context(clean_context(self._context))
data_to_url = {}
def base64_to_boundary(match):
key = match.group(2)
+5 -1
View File
@@ -716,7 +716,7 @@ class MailThread(models.AbstractModel):
for model in [bl_model for bl_model in bl_models if bl_model.model in self.env]: # transient test mode
rec_bounce_w_email = self.env[model.model].sudo().search([('email_normalized', '=', bounced_email)])
rec_bounce_w_email._message_receive_bounce(bounced_email, bounced_partner)
bounced_record_done = bool(bounced_record and model.model == bounced_model and bounced_record in rec_bounce_w_email)
bounced_record_done = bounced_record_done or (bounced_record and model.model == bounced_model and bounced_record in rec_bounce_w_email)
# set record as bounced unless already done due to blacklist mixin
if bounced_record and not bounced_record_done and issubclass(type(bounced_record), self.pool['mail.thread']):
@@ -889,6 +889,7 @@ class MailThread(models.AbstractModel):
raise TypeError('message must be an email.message.EmailMessage at this point')
catchall_alias = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias")
bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias")
bounce_alias_static = tools.str2bool(self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias.static", "False"))
fallback_model = model
# get email.message.Message variables for future processing
@@ -936,6 +937,9 @@ class MailThread(models.AbstractModel):
if bounce_match:
self._routing_handle_bounce(message, message_dict)
return []
if bounce_alias and bounce_alias_static and any(email == bounce_alias for email in email_to_localparts):
self._routing_handle_bounce(message, message_dict)
return []
if message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon':
self._routing_handle_bounce(message, message_dict)
return []
+4 -1
View File
@@ -382,6 +382,9 @@ class StockMove(models.Model):
qty_ratios = []
boms, bom_sub_lines = kit_bom.explode(product_id, kit_qty)
for bom_line, bom_line_data in bom_sub_lines:
# skip service since we never deliver them
if bom_line.product_id.type == 'service':
continue
bom_line_moves = self.filtered(lambda m: m.bom_line_id == bom_line)
if bom_line_moves:
if float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
@@ -399,7 +402,7 @@ class StockMove(models.Model):
outgoing_moves = bom_line_moves.filtered(filters['outgoing_moves'])
qty_processed = sum(incoming_moves.mapped('product_qty')) - sum(outgoing_moves.mapped('product_qty'))
# We compute a ratio to know how many kits we can produce with this quantity of that specific component
qty_ratios.append(qty_processed / qty_per_kit)
qty_ratios.append(float_round(qty_processed / qty_per_kit, precision_rounding=bom_line.product_id.uom_id.rounding))
else:
return 0.0
if qty_ratios:
+1
View File
@@ -73,6 +73,7 @@
Each operation is done at a specific Work Center and has a specific duration.
</p>
</field>
<field name="domain">['|', ('bom_id', '=', False), ('bom_id.active', '=', True)]</field>
</record>
<menuitem id="menu_mrp_routing_action"
+1 -1
View File
@@ -63,7 +63,7 @@ class MrpProduction(models.Model):
AccountAnalyticLine = self.env['account.analytic.line'].sudo()
for wc_line in self.workorder_ids.filtered('workcenter_id.costs_hour_account_id'):
vals = self._prepare_wc_analytic_line(wc_line)
precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding
precision_rounding = (wc_line.workcenter_id.costs_hour_account_id.currency_id or self.company_id.currency_id).rounding
if not float_is_zero(vals.get('amount', 0.0), precision_rounding=precision_rounding):
# we use SUPERUSER_ID as we do not guarantee an mrp user
# has access to account analytic lines but still should be
+7 -3
View File
@@ -52,9 +52,13 @@ class ProductProduct(models.Model):
value = 0
for move in stock_moves:
bom_line = move.bom_line_id
bom_line_data = bom_lines[bom_line]
bom_line_qty = bom_line_data['qty']
value += bom_line_qty * move.product_id._compute_average_price(qty_invoiced * bom_line_qty, qty_to_invoice * bom_line_qty, move)
if bom_line:
bom_line_data = bom_lines[bom_line]
line_qty = bom_line_data['qty']
else:
# bom was altered (i.e. bom line removed) after being used
line_qty = move.product_qty
value += line_qty * move.product_id._compute_average_price(qty_invoiced * line_qty, qty_to_invoice * line_qty, move)
return value
def _compute_bom_price(self, bom, boms_to_recompute=False):
@@ -9,7 +9,7 @@
"node":"*"
},
"author":{
"name":"FlectraHQ, Odoo S.A. - Hitesh Trivedi",
"name":"FlectraHQ, Inc., Odoo S.A. - Hitesh Trivedi",
"email":"thiteshm155@gmail.com"
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
'description': """
Auto-complete partner companies' data
""",
'author': "FlectraHQ",
'author': "FlectraHQ Inc..",
'category': 'Hidden/Tools',
'version': '1.0',
'depends': [
+8 -2
View File
@@ -39,9 +39,15 @@ class SipsController(http.Controller):
self.sips_validate_data(**post)
return ''
@http.route('/payment/sips/dpn', type='http', auth="public", methods=['POST'], csrf=False)
@http.route('/payment/sips/dpn', type='http', auth="public", methods=['POST'], csrf=False, save_session=False)
def sips_dpn(self, **post):
""" Sips DPN """
""" Sips DPN
The session cookie created by Flectra has not the attribute SameSite. Most of browsers will force this attribute
with the value 'Lax'. After the payment, Sips will perform a POST request on this route. For all these reasons,
the cookie won't be added to the request. As a result, if we want to save the session, the server will create
a new session cookie. Therefore, the previous session and all related information will be lost, so it will lead
to undesirable behaviors. This is the reason why `save_session=False` is needed.
"""
try:
_logger.info('Beginning Sips DPN form_feedback with post data %s', pprint.pformat(post)) # debug
self.sips_validate_data(**post)
+15 -6
View File
@@ -178,6 +178,13 @@ class PosSession(models.Model):
if (company.period_lock_date and start_date <= company.period_lock_date) or (company.fiscalyear_lock_date and start_date <= company.fiscalyear_lock_date):
raise ValidationError(_("You cannot create a session before the accounting lock date."))
def _check_bank_statement_state(self):
for session in self:
closed_statement_ids = session.statement_ids.filtered(lambda x: x.state != "open")
if closed_statement_ids:
raise UserError(_("Some Cash Registers are already posted. Please reset them to new in order to close the session.\n"
"Cash Registers: %r", list(statement.name for statement in closed_statement_ids)))
@api.model
def create(self, values):
config_id = values.get('config_id') or self.env.context.get('default_config_id')
@@ -248,8 +255,7 @@ class PosSession(models.Model):
if session.config_id.cash_control and not session.rescue:
last_sessions = self.env['pos.session'].search([('config_id', '=', self.config_id.id)]).ids
# last session includes the new one already.
if len(last_sessions) > 1:
self.cash_register_id.balance_start = self.env['pos.session'].browse(last_sessions[1]).cash_register_id.balance_end_real
self.cash_register_id.balance_start = self.env['pos.session'].browse(last_sessions[1]).cash_register_id.balance_end_real if len(last_sessions) > 1 else 0
values['state'] = 'opening_control'
else:
values['state'] = 'opened'
@@ -281,6 +287,7 @@ class PosSession(models.Model):
# Session without cash payment method will not have a cash register.
# However, there could be other payment methods, thus, session still
# needs to be validated.
self._check_bank_statement_state()
if not self.cash_register_id:
return self._validate_session()
@@ -322,10 +329,12 @@ class PosSession(models.Model):
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
else:
self.move_id.unlink()
elif not self.cash_register_id.difference:
cash_register = self.cash_register_id.sudo() if sudo else self.cash_register_id
cash_register.pos_session_id = False
cash_register.unlink()
else:
statement = self.cash_register_id
if not self.config_id.cash_control:
statement.write({'balance_end_real': statement.balance_end})
statement.button_post()
statement.button_validate()
self.write({'state': 'closed'})
return {
'type': 'ir.actions.client',
+5 -5
View File
@@ -2991,17 +2991,17 @@ exports.Order = Backbone.Model.extend({
line.set_quantity(options.quantity);
}
if(options.price !== undefined){
line.set_unit_price(options.price);
this.fix_tax_included_price(line);
}
if (options.price_extra !== undefined){
line.price_extra = options.price_extra;
line.set_unit_price(line.product.get_price(this.pricelist, line.get_quantity(), options.price_extra));
this.fix_tax_included_price(line);
}
if(options.price !== undefined){
line.set_unit_price(options.price);
this.fix_tax_included_price(line);
}
if(options.lst_price !== undefined){
line.set_lst_price(options.lst_price);
}
@@ -584,3 +584,79 @@ class TestPoSBasicConfig(TestPoSCommon):
self.assertEqual(len(other_customer_invoice_receivable_counterpart), 1, msg='there should one aggregated invoice receivable counterpart for self.other_customer')
self.assertEqual(bool(other_customer_invoice_receivable_counterpart.full_reconcile_id), True, msg='the aggregated receivable for self.other_customer should be reconciled')
self.assertEqual(other_customer_invoice_receivable_counterpart.balance, -200, msg='aggregated balance should be -200')
def test_cash_register_if_no_order(self):
# Process one order with product3
self.open_new_session()
session = self.pos_session
orders = []
order_data = self.create_ui_order_data([(self.product3, 1)])
amount_paid = order_data['data']['amount_paid']
self.env['pos.order'].create_from_ui([order_data])
session.action_pos_session_closing_control()
cash_register = session.cash_register_id
self.assertEqual(cash_register.balance_start, 0)
self.assertEqual(cash_register.balance_end_real, amount_paid)
# Open/Close session without any order
self.open_new_session()
session = self.pos_session
session.action_pos_session_closing_control()
cash_register = session.cash_register_id
self.assertEqual(cash_register.balance_start, amount_paid)
self.assertEqual(cash_register.balance_end_real, amount_paid)
self.assertEqual(self.config.last_session_closing_cash, amount_paid)
# Open/Close session with cash control and without any order
self.config.cash_control = True
self.open_new_session()
session = self.pos_session
session.set_cashbox_pos(amount_paid, False)
session.action_pos_session_closing_control()
self.env['account.bank.statement.cashbox'].create([{
'start_bank_stmt_ids': [],
'end_bank_stmt_ids': [(4, session.cash_register_id.id,)],
'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': amount_paid})],
'is_a_template': False
}])
session.action_pos_session_validate()
self.assertEqual(cash_register.balance_start, amount_paid)
self.assertEqual(cash_register.balance_end_real, amount_paid)
self.assertEqual(self.config.last_session_closing_cash, amount_paid)
def test_start_balance_with_two_pos(self):
""" When having several POS with cash control, this tests ensures that each POS has its correct opening amount """
def open_and_check(pos_data):
self.config = pos_data['config']
self.open_new_session()
session = self.pos_session
self.assertEqual(session.cash_register_id.balance_start, pos_data['amount_paid'])
session.set_cashbox_pos(pos_data['amount_paid'], False)
self.config.cash_control = True
pos01_config = self.config
pos02_config = pos01_config.copy()
pos01_data = {'config': pos01_config, 'p_qty': 1, 'amount_paid': 0}
pos02_data = {'config': pos02_config, 'p_qty': 3, 'amount_paid': 0}
for pos_data in [pos01_data, pos02_data]:
open_and_check(pos_data)
session = self.pos_session
order_data = self.create_ui_order_data([(self.product3, pos_data['p_qty'])])
pos_data['amount_paid'] += order_data['data']['amount_paid']
self.env['pos.order'].create_from_ui([order_data])
session.action_pos_session_closing_control()
self.env['account.bank.statement.cashbox'].create([{
'start_bank_stmt_ids': [],
'end_bank_stmt_ids': [(4, session.cash_register_id.id,)],
'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': pos_data['amount_paid']})],
'is_a_template': False
}])
session.action_pos_session_validate()
open_and_check(pos01_data)
open_and_check(pos02_data)
+3
View File
@@ -410,6 +410,9 @@ class CustomerPortal(Controller):
if not isinstance(report_sudo, type(request.env['ir.actions.report'])):
raise UserError(_("%s is not the reference of a report", report_ref))
if hasattr(model, 'company_id'):
report_sudo = report_sudo.with_company(model.company_id)
method_name = '_render_qweb_%s' % (report_type)
report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0]
reporthttpheaders = [
@@ -112,7 +112,7 @@ tour.register('purchase_tour' , {
trigger: ".modal-footer button[name='action_send_mail']",
extra_trigger: ".modal-footer button[name='action_send_mail']",
content: _t("Send the request for quotation to your vendor."),
position: "bottom",
position: "left",
run: 'click',
}, {
trigger: ".ui-sortable-handle",
+2 -9
View File
@@ -31,17 +31,10 @@ class PurchaseOrder(models.Model):
help="Completion date of the first receipt order.")
on_time_rate = fields.Float(related='partner_id.on_time_rate', compute_sudo=False)
@api.depends('order_line.move_ids.returned_move_ids',
'order_line.move_ids.state',
'order_line.move_ids.picking_id')
@api.depends('order_line.move_ids.picking_id')
def _compute_picking(self):
for order in self:
pickings = self.env['stock.picking']
for line in order.order_line:
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
# do some recursive search, but that could be prohibitive if not done correctly.
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
pickings |= moves.mapped('picking_id')
pickings = order.order_line.mapped('move_ids.picking_id')
order.picking_ids = pickings
order.picking_count = len(pickings)
+10 -2
View File
@@ -2,6 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from datetime import timedelta, datetime, time
from collections import defaultdict
from flectra import api, fields, models
@@ -20,12 +21,19 @@ class ResPartner(models.Model):
('partner_id', 'in', self.ids),
('date_order', '>', fields.Date.today() - timedelta(365)),
('qty_received', '!=', 0),
]).filtered(lambda l: l.product_id.sudo().product_tmpl_id.type != 'service' and l.order_id.state in ['done', 'purchase'])
('order_id.state', 'in', ['done', 'purchase'])
]).filtered(lambda l: l.product_id.sudo().product_tmpl_id.type != 'service')
lines_qty_done = defaultdict(lambda: 0)
moves = self.env['stock.move'].search([
('purchase_line_id', 'in', order_lines.ids),
('state', '=', 'done')]).filtered(lambda m: m.date.date() <= m.purchase_line_id.date_planned.date())
for move, qty_done in zip(moves, moves.mapped('quantity_done')):
lines_qty_done[move.purchase_line_id.id] += qty_done
partner_dict = {}
for line in order_lines:
on_time, ordered = partner_dict.get(line.partner_id, (0, 0))
ordered += line.product_uom_qty
on_time += sum(line.mapped('move_ids').filtered(lambda m: m.state == 'done' and m.date.date() <= m.purchase_line_id.date_planned.date()).mapped('quantity_done'))
on_time += lines_qty_done[line.id]
partner_dict[line.partner_id] = (on_time, ordered)
seen_partner = self.env['res.partner']
for partner, numbers in partner_dict.items():
+1 -1
View File
@@ -150,7 +150,7 @@ class StockRule(models.Model):
delay, delay_description = super()._get_lead_days(product)
bypass_delay_description = self.env.context.get('bypass_delay_description')
buy_rule = self.filtered(lambda r: r.action == 'buy')
seller = product._prepare_sellers()
seller = product.with_company(buy_rule.company_id)._select_seller()
if not buy_rule or not seller:
return delay, delay_description
buy_rule.ensure_one()
@@ -273,6 +273,9 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
def test_reordering_days_to_purchase(self):
company = self.env.ref('base.main_company')
company2 = self.env['res.company'].create({
'name': 'Second Company',
})
company.write({'po_lead': 0.00})
self.patcher = patch('flectra.addons.stock.models.stock_orderpoint.fields.Date', wraps=fields.Date)
self.mock_date = self.patcher.start()
@@ -280,19 +283,30 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
vendor = self.env['res.partner'].create({
'name': 'Colruyt'
})
vendor2 = self.env['res.partner'].create({
'name': 'Delhaize'
})
self.env.company.days_to_purchase = 2.0
product = self.env['product.product'].create({
'name': 'Chicory',
'type': 'product',
'seller_ids': [(0, 0, {'name': vendor.id, 'delay': 1.0})]
'seller_ids': [
(0, 0, {'name': vendor2.id, 'delay': 15.0, 'company_id': company2.id}),
(0, 0, {'name': vendor.id, 'delay': 1.0, 'company_id': company.id})
]
})
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = product
orderpoint_form.product_min_qty = 0.0
orderpoint = orderpoint_form.save()
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'].with_company(company2))
orderpoint_form.product_id = product
orderpoint_form.product_min_qty = 0.0
orderpoint = orderpoint_form.save()
warehouse = self.env['stock.warehouse'].search([], limit=1)
delivery_moves = self.env['stock.move']
for i in range(0, 6):
+1 -1
View File
@@ -28,7 +28,7 @@ tour.register("sale_tour", {
}, {
trigger: ".modal-content button[name='action_save_onboarding_company_step']",
content: _t("Looks good. Let's continue."),
position: "bottom",
position: "left",
}, {
trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_base_document_layout]',
extra_trigger: ".o_sale_order",
+2 -2
View File
@@ -17,7 +17,7 @@ class SalePaymentLink(models.TransientModel):
record = self.env[res['res_model']].browse(res['res_id'])
res.update({
'description': record.name,
'amount': record.amount_total - sum(record.invoice_ids.mapped('amount_total')),
'amount': record.amount_total - sum(record.invoice_ids.filtered(lambda x: x.state != 'cancel').mapped('amount_total')),
'currency_id': record.currency_id.id,
'partner_id': record.partner_id.id,
'amount_max': record.amount_total
@@ -46,4 +46,4 @@ class SalePaymentLink(models.TransientModel):
payment_link.access_token
)
else:
super(SalePaymentLink, payment_link)._generate_link()
super(SalePaymentLink, payment_link)._generate_link()
+6
View File
@@ -467,6 +467,12 @@ class SaleOrder(models.Model):
return self.env['sale.order.line'].browse()
return invoiceable_lines
def update_prices(self):
"""Recompute coupons/promotions after pricelist prices reset."""
super().update_prices()
if any(line.is_reward_line for line in self.order_line):
self.recompute_coupon_lines()
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
+1 -1
View File
@@ -25,7 +25,7 @@ class AccountMoveLine(models.Model):
prod_qty_invoiced = factor * qty_invoiced
prod_qty_to_invoice = factor * qty_to_invoice
average_price_unit += factor * product._compute_average_price(prod_qty_invoiced, prod_qty_to_invoice, prod_moves)
price_unit = average_price_unit or price_unit
price_unit = average_price_unit / bom.product_qty or price_unit
price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom_id)
return price_unit
+1 -1
View File
@@ -115,7 +115,7 @@ class SaleOrderLine(models.Model):
for line, line_data in lines:
product = line.product_id.id
uom = line.product_uom_id
qty = line.product_qty
qty = line_data['qty']
if components.get(product, False):
if uom.id != components[product]['uom']:
from_uom = uom
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_sale_mrp_flow
from . import test_sale_mrp_kit_bom
from . import test_sale_mrp_lead_time
from . import test_sale_mrp_procurement
from . import test_multistep_manufacturing
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.tests.common import TransactionCase, Form
class TestSaleMrpKitBom(TransactionCase):
def _create_product(self, name, type, price):
return self.env['product.product'].create({
'name': name,
'type': type,
'standard_price': price,
})
def test_sale_mrp_kit_bom_cogs(self):
"""Check invoice COGS aml after selling and delivering a product
with Kit BoM having another product with Kit BoM as component"""
# ----------------------------------------------
# BoM of Kit A:
# - BoM Type: Kit
# - Quantity: 3
# - Components:
# * 2 x Kit B
# * 1 x Component A (Cost: $3)
#
# BoM of Kit B:
# - BoM Type: Kit
# - Quantity: 10
# - Components:
# * 2 x Component B (Cost: $4)
# * 3 x Component BB (Cost: $5)
# ----------------------------------------------
self.env.user.company_id.anglo_saxon_accounting = True
self.stock_input_account = self.env['account.account'].create({
'name': 'Stock Input',
'code': 'StockIn',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.stock_output_account = self.env['account.account'].create({
'name': 'Stock Output',
'code': 'StockOut',
'reconcile': True,
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.stock_valuation_account = self.env['account.account'].create({
'name': 'Stock Valuation',
'code': 'StockVal',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.expense_account = self.env['account.account'].create({
'name': 'Expense Account',
'code': 'Exp',
'user_type_id': self.env.ref('account.data_account_type_expenses').id,
})
self.income_account = self.env['account.account'].create({
'name': 'Income Account',
'code': 'Inc',
'user_type_id': self.env.ref('account.data_account_type_expenses').id,
})
self.stock_journal = self.env['account.journal'].create({
'name': 'Stock Journal',
'code': 'STJTEST',
'type': 'general',
})
self.recv_account = self.env['account.account'].create({
'name': 'account receivable',
'code': 'RECV',
'user_type_id': self.env.ref('account.data_account_type_receivable').id,
'reconcile': True,
})
self.pay_account = self.env['account.account'].create({
'name': 'account payable',
'code': 'PAY',
'user_type_id': self.env.ref('account.data_account_type_payable').id,
'reconcile': True,
})
self.customer = self.env['res.partner'].create({
'name': 'customer',
'property_account_receivable_id': self.recv_account.id,
'property_account_payable_id': self.pay_account.id,
})
self.journal_sale = self.env['account.journal'].create({
'name': 'Sale Journal - Test',
'code': 'AJ-SALE',
'type': 'sale',
'company_id': self.env.user.company_id.id,
})
self.component_a = self._create_product('Component A', 'consu', 3.00)
self.component_b = self._create_product('Component B', 'consu', 4.00)
self.component_bb = self._create_product('Component BB', 'consu', 5.00)
self.kit_a = self._create_product('Kit A', 'product', 0.00)
self.kit_b = self._create_product('Kit B', 'consu', 0.00)
self.kit_a.write({
'categ_id': self.env.ref('product.product_category_all').id,
'property_account_expense_id': self.expense_account.id,
'property_account_income_id': self.income_account.id,
})
self.kit_a.categ_id.write({
'property_stock_account_input_categ_id': self.stock_input_account.id,
'property_stock_account_output_categ_id': self.stock_output_account.id,
'property_stock_valuation_account_id': self.stock_valuation_account.id,
'property_stock_journal': self.stock_journal.id,
'property_valuation': 'real_time',
})
# Create BoM for Kit A
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.kit_a
bom_product_form.product_tmpl_id = self.kit_a.product_tmpl_id
bom_product_form.product_qty = 3.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.kit_b
bom_line.product_qty = 2.0
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.component_a
bom_line.product_qty = 1.0
self.bom_a = bom_product_form.save()
# Create BoM for Kit B
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.kit_b
bom_product_form.product_tmpl_id = self.kit_b.product_tmpl_id
bom_product_form.product_qty = 10.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.component_b
bom_line.product_qty = 2.0
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.component_bb
bom_line.product_qty = 3.0
self.bom_b = bom_product_form.save()
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kit_a.name,
'product_id': self.kit_a.id,
'product_uom_qty': 1.0,
'product_uom': self.kit_a.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
so.picking_ids.move_lines.quantity_done = 1
so.picking_ids.button_validate()
invoice = so.with_context(default_journal_id=self.journal_sale.id)._create_invoices()
invoice.action_post()
# Check the resulting accounting entries
amls = invoice.line_ids
self.assertEqual(len(amls), 4)
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.stock_output_account)
self.assertEqual(stock_out_aml.debit, 0)
self.assertAlmostEqual(stock_out_aml.credit, 2.53)
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.expense_account)
self.assertAlmostEqual(cogs_aml.debit, 2.53)
self.assertEqual(cogs_aml.credit, 0)
@@ -181,8 +181,7 @@ class ProfitabilityAnalysis(models.Model):
AND NOT EXISTS (
SELECT SOL.id
FROM sale_order_line SOL
JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOL.id
JOIN account_move_line AML ON SOINV.invoice_line_id = AAL.move_id
JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOL.id AND SOINV.invoice_line_id = AAL.move_id -- AAL.move_id is an account.move.line id
WHERE SOL.qty_delivered_method IN ('timesheet', 'manual')
OR (SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no')
)
@@ -10,6 +10,7 @@ const SoLineOne2Many = FieldOne2Many.extend({
ev.data.changes &&
ev.data.changes.hasOwnProperty('timesheet_ids') &&
ev.data.changes.timesheet_ids.operation === 'UPDATE' &&
ev.data.changes.timesheet_ids.data &&
ev.data.changes.timesheet_ids.data.hasOwnProperty('so_line')) {
const line = this.value.data.find(line => {
return line.id === ev.data.changes.timesheet_ids.id;
+1 -1
View File
@@ -69,7 +69,7 @@ class TestSalesMC(TestSalesCommon):
cls.company_2 = cls.env['res.company'].create({
'name': 'New Test Company',
'email': 'company.2@test.example.com',
'country_id': cls.env.ref('base.fr'),
'country_id': cls.env.ref('base.fr').id,
})
cls.team_c2 = cls.env['crm.team'].create({
'name': 'C2 Team1',
+1 -1
View File
@@ -8,7 +8,7 @@
The purpose of this technical module is to provide a front for
social media configuration for any other module that might need it.
""",
'author': "FlectraHQ, Odoo S.A.",
'author': "FlectraHQ Inc.., Odoo S.A.",
'category': 'Hidden',
'version': '0.1',
'depends': ['base'],
+1 -1
View File
@@ -18,7 +18,7 @@ class ResConfigSettings(models.TransientModel):
group_stock_production_lot = fields.Boolean("Lots & Serial Numbers",
implied_group='stock.group_production_lot')
group_lot_on_delivery_slip = fields.Boolean("Display Lots & Serial Numbers on Delivery Slips",
implied_group='stock.group_lot_on_delivery_slip')
implied_group='stock.group_lot_on_delivery_slip', group="base.group_user,base.group_portal")
group_stock_tracking_lot = fields.Boolean("Packages",
implied_group='stock.group_tracking_lot')
group_stock_tracking_owner = fields.Boolean("Consignment",
+4
View File
@@ -572,6 +572,8 @@ class InventoryLine(models.Model):
result = False
else:
raise NotImplementedError()
if not self.env.context.get('default_inventory_id'):
raise NotImplementedError(_('Unsupported search on %s outside of an Inventory Adjustment', 'difference_qty'))
lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))])
line_ids = lines.filtered(lambda line: float_is_zero(line.difference_qty, line.product_id.uom_id.rounding) == result).ids
return [('id', 'in', line_ids)]
@@ -582,6 +584,8 @@ class InventoryLine(models.Model):
value = not value
else:
raise NotImplementedError()
if not self.env.context.get('default_inventory_id'):
raise NotImplementedError(_('Unsupported search on %s outside of an Inventory Adjustment', 'outdated'))
lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))])
line_ids = lines.filtered(lambda line: line.outdated == value).ids
return [('id', 'in', line_ids)]
+3 -3
View File
@@ -327,12 +327,12 @@ class StockMove(models.Model):
move.quantity_done = quantity_done
else:
# compute
move_lines = self.env['stock.move.line']
move_lines_ids = set()
for move in self:
move_lines |= move._get_move_lines()
move_lines_ids |= set(move._get_move_lines().ids)
data = self.env['stock.move.line'].read_group(
[('id', 'in', move_lines.ids)],
[('id', 'in', list(move_lines_ids))],
['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'],
lazy=False
)
+1 -1
View File
@@ -198,7 +198,7 @@
<field name="location_id" options="{'no_create': True}" string="From" groups="stock.group_stock_multi_locations" optional="show"/>
<field name="location_dest_id" options="{'no_create': True}" string="To" groups="stock.group_stock_multi_locations" optional="show"/>
<field name="partner_id" optional="show"/>
<field name="signature" string="Signed" optional="hide" widget="boolean" groups="stock.group_stock_sign_delivery"/>
<field name="signature" string="Signed" optional="hide" groups="stock.group_stock_sign_delivery"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
<field name="scheduled_date" optional="show" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
+4 -4
View File
@@ -16,9 +16,9 @@ class ProductTemplate(models.Model):
def write(self, vals):
for product in self:
if (('type' in vals and vals['type'] != 'service') or ('landed_cost_ok' in vals and vals['landed_cost_ok'] == False)) and product.type == 'service' and product.landed_cost_ok:
if (('type' in vals and vals['type'] != 'service') or ('landed_cost_ok' in vals and not vals['landed_cost_ok'])) and product.type == 'service' and product.landed_cost_ok:
if self.env['account.move.line'].search_count([('product_id', 'in', product.product_variant_ids.ids), ('is_landed_costs_line', '=', True)]):
raise UserError(_("You cannot change the product type or disable landed cost option because the product is used in an account move line."))
else:
vals['landed_cost_ok'] = False
super(ProductTemplate, product).write(vals)
vals['landed_cost_ok'] = False
return super().write(vals)
+2 -2
View File
@@ -433,7 +433,7 @@ Résultat de recherche d'images pour "dessin la princesse au petit pois"
--
Soup
FlectraHQ, Odoo S.A.
FlectraHQ, Inc., Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: http://www.flectrahq.com
@@ -456,7 +456,7 @@ src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQjCNAadd3NDM8g9w0P_-g
<pre class="moz-signature" cols="72">--
Soup
FlectraHQ, Odoo S.A.
FlectraHQ, Inc., Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: <a class="moz-txt-link-freetext" href="http://www.flectrahq.com">http://www.flectrahq.com</a> </pre>
+68 -10
View File
@@ -258,6 +258,7 @@ class TestMailgateway(TestMailCommon):
set(message.attachment_ids.mapped('name')),
set(['rosaçée.gif', 'verte!µ.gif', 'orangée.gif']))
@mute_logger('flectra.addons.mail.models.mail_thread')
def test_message_process_followers(self):
""" Incoming email: recognized author not archived and not flectrabot: added as follower """
with self.mock_mail_gateway():
@@ -763,6 +764,48 @@ class TestMailgateway(TestMailCommon):
self.assertFalse(new_recs)
self.assertNotSentEmail()
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_route_bounce_if_static_but_still_has_plus_addressing(self):
"""Incoming email: bounce using bounce alias without plus addressing: keep old behavior."""
self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
with self.mock_mail_gateway():
new_recs = self.format_and_process(
MAIL_TEMPLATE, self.partner_1.email_formatted,
'%s+%s-%s-%s@%s' % (
self.alias_bounce, self.fake_email.id,
self.fake_email.model, self.fake_email.res_id,
self.alias_domain
),
subject='Should bounce',
)
self.assertFalse(new_recs)
self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails')
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_route_bounce_if_static_without_plus_addressing(self):
"""Incoming email: bounce using bounce alias without plus addressing: bounce it."""
self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
with self.mock_mail_gateway():
new_recs = self.format_and_process(
MAIL_TEMPLATE, self.partner_1.email_formatted,
'%s@%s' % (self.alias_bounce, self.alias_domain),
subject='Should bounce',
)
self.assertFalse(new_recs)
self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails')
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_route_no_bounce_if_not_static_without_plus_addressing(self):
"""Incoming email: bounce using bounce alias without plus addressing: raise as
considering as a direct write to bounce alias -> invalid """
self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', False)
with self.assertRaises(ValueError):
self.format_and_process(
MAIL_TEMPLATE, self.partner_1.email_formatted,
'%s@%s' % (self.alias_bounce, self.alias_domain),
subject="Should fail because it is not a bounce and there's no alias",
)
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_route_bounce_other_recipients(self):
"""Incoming email: bounce processing: bounce should be computed even if not first recipient """
@@ -905,24 +948,24 @@ class TestMailgateway(TestMailCommon):
@mute_logger('flectra.addons.mail.models.mail_thread')
def test_message_process_bounce_records_channel(self):
""" Test blacklist allow to multi-bounce and auto update of mail.channel """
self.other_record = self.env['mail.test.gateway'].create({
other_record = self.env['mail.test.gateway'].create({
'email_from': 'Another name <%s>' % self.partner_1.email
})
self.yet_other_record = self.env['mail.test.gateway'].create({
yet_other_record = self.env['mail.test.gateway'].create({
'email_from': 'Yet Another name <%s>' % self.partner_1.email.upper()
})
self.test_channel = self.env['mail.channel'].create({
test_channel = self.env['mail.channel'].create({
'name': 'Test',
'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.partner_1.id})],
})
self.fake_email.write({
'model': 'mail.channel',
'res_id': self.test_channel.id,
'res_id': test_channel.id,
})
self.assertIn(self.partner_1, self.test_channel.channel_partner_ids)
self.assertIn(self.partner_1, test_channel.channel_partner_ids)
self.assertEqual(self.partner_1.message_bounce, 0)
self.assertEqual(self.other_record.message_bounce, 0)
self.assertEqual(self.yet_other_record.message_bounce, 0)
self.assertEqual(other_record.message_bounce, 0)
self.assertEqual(yet_other_record.message_bounce, 0)
extra = self.fake_email.message_id
for i in range(10):
@@ -930,9 +973,24 @@ class TestMailgateway(TestMailCommon):
self.assertFalse(record)
self.assertEqual(self.partner_1.message_bounce, 10)
self.assertEqual(self.test_record.message_bounce, 0)
self.assertEqual(self.other_record.message_bounce, 10)
self.assertEqual(self.yet_other_record.message_bounce, 10)
self.assertNotIn(self.partner_1, self.test_channel.channel_partner_ids)
self.assertEqual(other_record.message_bounce, 10)
self.assertEqual(yet_other_record.message_bounce, 10)
self.assertNotIn(self.partner_1, test_channel.channel_partner_ids)
@mute_logger('flectra.addons.mail.models.mail_thread')
def test_message_process_bounce_records_partner(self):
""" Test blacklist + bounce on ``res.partner`` model """
self.assertEqual(self.partner_1.message_bounce, 0)
self.fake_email.write({
'model': 'res.partner',
'res_id': self.partner_1.id,
})
extra = self.fake_email.message_id
record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra)
self.assertFalse(record)
self.assertEqual(self.partner_1.message_bounce, 1)
self.assertEqual(self.test_record.message_bounce, 0)
# --------------------------------------------------
# Thread formation
+51 -2
View File
@@ -4,14 +4,24 @@
import psycopg2
from flectra import api
from flectra.addons.base.models.ir_mail_server import MailDeliveryException
from flectra.addons.test_mail.tests.common import TestMailCommon
from flectra.tests import common
from flectra.tests import common, tagged
from flectra.tools import mute_logger
@tagged('mail_mail')
class TestMailMail(TestMailCommon):
@classmethod
def setUpClass(cls):
super(TestMailMail, cls).setUpClass()
cls._init_mail_gateway()
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
'name': 'Test',
'email_from': 'ignasse@example.com',
}).with_context({})
@mute_logger('flectra.addons.mail.models.mail_mail')
def test_mail_message_notify_from_mail_mail(self):
# Due ot post-commit hooks, store send emails in every step
@@ -25,6 +35,45 @@ class TestMailMail(TestMailCommon):
self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
self.assertEqual(len(self._mails), 1)
def test_mail_mail_return_path(self):
# mail without thread-enabled record
base_values = {
'body_html': '<p>Test</p>',
'email_to': 'test@example.com',
}
mail = self.env['mail.mail'].create(base_values)
with self.mock_mail_gateway():
mail.send()
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d@%s' % (self.alias_bounce, mail.id, self.alias_domain))
# mail on thread-enabled record
mail = self.env['mail.mail'].create(dict(base_values, **{
'model': self.test_record._name,
'res_id': self.test_record.id,
}))
with self.mock_mail_gateway():
mail.send()
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d-%s-%s@%s' % (self.alias_bounce, mail.id, self.test_record._name, self.test_record.id, self.alias_domain))
# force static addressing on bounce alias
self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
# mail without thread-enabled record
mail = self.env['mail.mail'].create(base_values)
with self.mock_mail_gateway():
mail.send()
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
# mail on thread-enabled record
mail = self.env['mail.mail'].create(dict(base_values, **{
'model': self.test_record._name,
'res_id': self.test_record.id,
}))
with self.mock_mail_gateway():
mail.send()
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
class TestMailMailRace(common.TransactionCase):
+14 -7
View File
@@ -148,19 +148,26 @@ class UoM(models.Model):
- if true, raise an exception if the conversion is not possible (different UoM category),
- otherwise, return the initial quantity
"""
if not self:
if not self or not qty:
return qty
self.ensure_one()
if self.category_id.id != to_unit.category_id.id:
if self != to_unit and self.category_id.id != to_unit.category_id.id:
if raise_if_failure:
raise UserError(_('The unit of measure %s defined on the order line doesn\'t belong to the same category as the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.') % (self.name, to_unit.name))
else:
return qty
amount = qty / self.factor
if to_unit:
amount = amount * to_unit.factor
if round:
amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
if self == to_unit:
amount = qty
else:
amount = qty / self.factor
if to_unit:
amount = amount * to_unit.factor
if to_unit and round:
amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
return amount
def _compute_price(self, price, to_unit):
+1 -1
View File
@@ -1772,7 +1772,7 @@ class Export(http.Controller):
fields = self.fields_get(model)
if import_compat:
if parent_field_type in ['many2one', 'many2many']:
rec_name = request.env[model]._rec_name
rec_name = request.env[model]._rec_name_fallback()
fields = {'id': fields['id'], rec_name: fields[rec_name]}
else:
fields['.id'] = {**fields['id']}
@@ -235,6 +235,9 @@ var FormRenderer = BasicRenderer.extend({
*/
setLocalState: function (state) {
for (const notebook of this.el.querySelectorAll(':scope div.o_notebook')) {
if (notebook.closest(".o_field_widget")) {
continue;
}
const name = notebook.dataset.name;
if (name in state) {
const navs = notebook.querySelectorAll(':scope .o_notebook_headers .nav-item');
@@ -172,8 +172,8 @@ var KanbanRecord = Widget.extend({
* @returns {string} the url of the image
*/
_getImageURL: function (model, field, id, placeholder) {
id = (_.isArray(id) ? id[0] : id) || false;
var isCurrentRecord = this.modelName === model && this.recordData.id === id;
id = (_.isArray(id) ? id[0] : id) || null;
var isCurrentRecord = this.modelName === model && (this.recordData.id === id || (!this.recordData.id && !id));
var url;
if (isCurrentRecord && this.record[field] && this.record[field].raw_value && !utils.is_bin_size(this.record[field].raw_value)) {
// Use magic-word technique for detecting image type
@@ -21,7 +21,9 @@ var RecordQuickCreate = Widget.extend({
'click .o_kanban_add': '_onAddClicked',
'click .o_kanban_edit': '_onEditClicked',
'click .o_kanban_cancel': '_onCancelClicked',
'mousedown': '_onMouseDown',
},
mouseDownInside: false,
/**
* @override
@@ -255,6 +257,9 @@ var RecordQuickCreate = Widget.extend({
* @param {MouseEvent} ev
*/
_onWindowClicked: function (ev) {
var mouseDownInside = this.mouseDownInside;
this.mouseDownInside = false;
// ignore clicks if the quick create is not in the dom
if (!document.contains(this.el)) {
return;
@@ -288,12 +293,21 @@ var RecordQuickCreate = Widget.extend({
}
// ignore clicks if target is inside the quick create
if (this.el.contains(ev.target) || this.el === ev.target) {
if (this.el.contains(ev.target) || this.el === ev.target || mouseDownInside) {
return;
}
this.cancel();
},
/**
* Detects if the click is originally from the quick create
*
* @private
* @param {MouseEvent} ev
*/
_onMouseDown: function(ev){
this.mouseDownInside = true;
}
});
return RecordQuickCreate;
@@ -100,6 +100,10 @@
display: inline-block;
}
}
.o_field_color_picker_preview > li > a {
cursor: default;
}
}
.o_form_uri {
+44 -1
View File
@@ -936,7 +936,7 @@ QUnit.module('Views', {
});
QUnit.test('quick create record: cancel and validate without using the buttons', async function (assert) {
assert.expect(8);
assert.expect(9);
var nbRecords = 4;
var kanban = await createView({
@@ -972,6 +972,14 @@ QUnit.module('Views', {
assert.containsNone(kanban, '.o_kanban_quick_create',
"the quick create should be destroyed when the user clicks outside");
// click to input and drag the mouse outside, should not cancel the quick creation
await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first());
$quickCreate = kanban.$('.o_kanban_quick_create');
await testUtils.dom.triggerMouseEvent($quickCreate.find('input'), 'mousedown');
await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first').first());
assert.containsOnce(kanban, '.o_kanban_quick_create',
"the quick create should not have been destroyed after clicking outside");
// click to really add an element
await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first());
$quickCreate = kanban.$('.o_kanban_quick_create');
@@ -6384,6 +6392,41 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test("test displaying image from m2o field (m2o field not set)", async function (assert) {
assert.expect(2);
this.data.foo_partner = {
fields: {
name: {string: "Foo Name", type: "char"},
partner_id: {string: "Partner", type: "many2one", relation: "partner"},
},
records: [
{id: 1, name: 'foo_with_partner_image', partner_id: 1},
{id: 2, name: 'foo_no_partner'},
]
};
const kanban = await createView({
View: KanbanView,
model: "foo_partner",
data: this.data,
arch: `
<kanban>
<templates>
<div t-name="kanban-box">
<field name="name"/>
<field name="partner_id"/>
<img t-att-src="kanban_image('partner', 'image', record.partner_id.raw_value)"/>
</div>
</templates>
</kanban>`,
});
assert.containsOnce(kanban, 'img[data-src*="/web/image"][data-src$="&id=1"]', "image url should contain id of set partner_id");
assert.containsOnce(kanban, 'img[data-src*="/web/image"][data-src$="&id="]', "image url should contain an empty id if partner_id is not set");
kanban.destroy();
});
QUnit.test('check if the view destroys all widgets and instances', async function (assert) {
assert.expect(2);
@@ -37,6 +37,24 @@ QUnit.module('web_editor', {}, function () {
display_name: "first record",
header: "<p> &nbsp;&nbsp; <br> </p>",
body: "<p>toto toto toto</p><p>tata</p>",
}, {
id: 2,
display_name: "second record",
header: "<p> &nbsp;&nbsp; <br> </p>",
body: `
<div class="o_form_sheet_bg">
<div class="clearfix position-relative o_form_sheet" style="width: 1140px;">
<div class="o_notebook">
<div class="tab-content">
<div class="tab-pane active" id="notebook_page_820">
<div class="oe_form_field oe_form_field_html o_field_widget" name="description" style="margin-bottom: 5px;">
hacky code to test
</div>
</div>
</div>
</div>
</div>
</div>`,
}],
},
'mass.mailing': {
@@ -132,6 +150,26 @@ QUnit.module('web_editor', {}, function () {
form.destroy();
});
QUnit.test('notebooks defined inside HTML field widgets are ignored when calling setLocalState', async function (assert) {
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 2,
});
// check that there is no error on clicking Edit
await testUtils.form.clickEdit(form);
await testUtils.nextTick();
assert.containsOnce(form, '.o_form_editable');
form.destroy();
});
QUnit.test('check if required field is set', async function (assert) {
assert.expect(1);
+4 -1
View File
@@ -151,9 +151,12 @@ class Website(models.Model):
# don't add child menu if parent is forbidden
if menu.parent_id and menu.parent_id in menus:
menu.parent_id._cache['child_id'] += (menu.id,)
# prefetch every website.page and ir.ui.view at once
menus.mapped('is_visible')
website.menu_id = menus and menus.filtered(lambda m: not m.parent_id)[0].id or False
top_menus = menus.filtered(lambda m: not m.parent_id)
website.menu_id = top_menus and top_menus[0].id or False
# self.env.uid for ir.rule groups on menu
@tools.ormcache('self.env.uid', 'self.id')
@@ -965,9 +965,9 @@ registry.ScrollButton = registry.anchorSlide.extend({
*/
_onAnimateClick: function (ev) {
ev.preventDefault();
const $nextSection = this.$el.closest('section').next('section');
if ($nextSection.length) {
this._scrollTo($nextSection);
const $nextElement = this.$el.closest('section').next();
if ($nextElement.length) {
this._scrollTo($nextElement);
}
},
});
@@ -70,17 +70,45 @@ weSnippetEditor.Class.include({
size: 'medium',
title: _t("Google Map API Key"),
buttons: [
{text: _t("Save"), classes: 'btn-primary', close: true, click: async () => {
const newAPIKey = dialog.$('#api_key_input').val() || false;
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: newAPIKey},
],
});
invalidated = true;
{text: _t("Save"), classes: 'btn-primary', click: async (ev) => {
const $apiKeyInput = dialog.$('#api_key_input');
const valueAPIKey = $apiKeyInput.val();
const $apiKeyHelp = dialog.$('#api_key_help');
if (!valueAPIKey) {
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.text(_t("Enter an API Key"));
return;
}
const $button = $(ev.currentTarget);
$button.prop('disabled', true);
try {
const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${valueAPIKey}`);
if (response.status === 200) {
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: valueAPIKey},
],
});
invalidated = true;
dialog.close();
} else {
const text = await response.text();
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.empty().text(
_t("Invalid API Key. The following error was returned by Google:")
).append($('<i/>', {
text: text,
class: 'ml-1',
}));
}
} catch (e) {
$apiKeyHelp.text(_t("Check your connection and try again"));
} finally {
$button.prop("disabled", false);
}
}},
{text: _t("Cancel"), close: true}
],
+9 -2
View File
@@ -1073,8 +1073,15 @@ header {
}
}
}
body.o_connected_user:not(.editor_has_snippets) #wrapwrap > header {
top: $o-navbar-height;
body.o_connected_user {
&:not(.editor_has_snippets) #wrapwrap > header {
top: $o-navbar-height;
}
&.editor_has_snippets #wrapwrap > header {
@if $-hamburger-right {
right: $o-we-sidebar-width;
}
}
}
}
} @else if o-website-value('header-template') == 'boxed' {
@@ -121,6 +121,8 @@
t-att-value="apiKey or ''"
placeholder="BSgzTvR5L1GB9jriT451iTN4huVPxHmltG6T6eo"/>
</div>
<small id="api_key_help" class="text-danger">
</small>
<div class="small form-text text-muted">
Hint: How to use Google Map on your website (Contact Us page and as a snippet)
<br/>
+4 -1
View File
@@ -116,7 +116,10 @@ class WebsiteBlog(http.Controller):
step=self._blog_post_per_page,
)
all_tags = blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag'])
if not blogs:
all_tags = request.env['blog.tag']
else:
all_tags = blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag'])
tag_category = sorted(all_tags.mapped('category_id'), key=lambda category: category.name.upper())
other_tags = sorted(all_tags.filtered(lambda x: not x.category_id), key=lambda tag: tag.name.upper())
@@ -21,7 +21,7 @@ flectra.define('website_crm.tour', function(require) {
}, {
content: "Complete Company",
trigger: "input[name=partner_name]",
run: "text FlectraHQ, Odoo S.A."
run: "text FlectraHQ, Inc., Odoo S.A."
}, {
content: "Complete Subject",
trigger: "input[name=name]",
+1 -1
View File
@@ -15,4 +15,4 @@ class TestWebsiteCrm(flectra.tests.HttpCase):
self.assertEqual(len(record), 1)
self.assertEqual(record.contact_name, 'John Smith')
self.assertEqual(record.email_from, 'john@smith.com')
self.assertEqual(record.partner_name, 'FlectraHQ, Odoo S.A.')
self.assertEqual(record.partner_name, 'FlectraHQ, Inc., Odoo S.A.')
@@ -4,7 +4,7 @@
import random
from flectra import api, fields, models, _
from flectra.exceptions import AccessDenied, AccessError
from flectra.exceptions import AccessDenied, AccessError, UserError
from flectra.tools import html_escape
@@ -237,6 +237,14 @@ class CrmLead(models.Model):
})
lead.write(lead_values)
def update_contact_details_from_portal(self, values):
self.check_access_rights('write')
fields = ['partner_name', 'phone', 'mobile', 'email_from', 'street', 'street2',
'city', 'zip', 'state_id', 'country_id']
if any([key not in fields for key in values]):
raise UserError(_("Not allowed to update the following field(s) : %s.") % ", ".join([key for key in values if not key in fields]))
return self.sudo().write(values)
@api.model
def create_opp_portal(self, values):
if not (self.env.user.partner_id.grade_id or self.env.user.commercial_partner_id.grade_id):
@@ -93,7 +93,7 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
_editContact: function () {
return this._rpc({
model: 'crm.lead',
method: 'write',
method: 'update_contact_details_from_portal',
args: [[parseInt($('.edit_contact_form .opportunity_id').val())], {
partner_name: $('.edit_contact_form .partner_name').val(),
phone: $('.edit_contact_form .phone').val(),
@@ -138,7 +138,7 @@ Camptocamp has developed a strong expertise in the following areas:
<li>PrestaShop Flectra connector;</li></ul></li>
<li>development and implementation of business applications 100% specific leveraging the power of the Flectra framework (OpenObject).</li></ul>
<br />
In addition, thanks to its heavy involvement in the ecosystem Flectra for several years, has been promoted by Camptocamp Editor FlectraHQ status Flectra best contributor in 2012 and 2013.<br/>
In addition, thanks to its heavy involvement in the ecosystem Flectra for several years, has been promoted by Camptocamp Editor FlectraHQ, Inc. status Flectra best contributor in 2012 and 2013.<br/>
]]></field>
</record>
-3
View File
@@ -532,9 +532,6 @@ class WebsiteSale(http.Controller):
partner_id = int(kw.get('partner_id'))
if partner_id in shippings.mapped('id'):
order.partner_shipping_id = partner_id
elif not order.partner_shipping_id:
last_order = request.env['sale.order'].sudo().search([("partner_id", "=", order.partner_id.id)], order='id desc', limit=1)
order.partner_shipping_id.id = last_order and last_order.id
values = {
'order': order,
@@ -10,7 +10,7 @@ To configure product attributes, activate *Attributes & Variants* in the Website
Finally, the module comes with an option to display an attribute summary table in product web pages (available in Customize menu).
""",
'author': 'FlectraHQ',
'author': 'FlectraHQ Inc..',
'category': 'Website/Website',
'version': '1.0',
'depends': ['website_sale'],
+1 -1
View File
@@ -6,7 +6,7 @@
'description': """
Allow shoppers of your eCommerce store to create personalized collections of products they want to buy and save them for future reference.
""",
'author': 'FlectraHQ',
'author': 'FlectraHQ Inc..',
'category': 'Website/Website',
'version': '1.0',
'depends': ['website_sale'],