mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-24 23:17:39 -05:00
[PATCH] Upstream patch
This commit is contained in:
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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
----------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"/> •
|
||||
</t>
|
||||
<t t-if="company.street">
|
||||
<span t-field="company.street"/> •
|
||||
</t>
|
||||
<t t-if="company.street2">
|
||||
<span t-field="company.street2"/> •
|
||||
</t>
|
||||
<t t-if="company.zip">
|
||||
<span t-field="company.zip"/> •
|
||||
</t>
|
||||
<t t-if="company.city">
|
||||
<span t-field="company.city"/> •
|
||||
</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'">
|
||||
&.din_page {
|
||||
&.header {
|
||||
.company_header {
|
||||
.name_container {
|
||||
color: <t t-esc='primary'/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
===============================================================
|
||||
|
||||
@@ -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':
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
'description': """
|
||||
Auto-complete partner companies' data
|
||||
""",
|
||||
'author': "FlectraHQ",
|
||||
'author': "FlectraHQ Inc..",
|
||||
'category': 'Hidden/Tools',
|
||||
'version': '1.0',
|
||||
'depends': [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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'))]}"/>
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> <br> </p>",
|
||||
body: "<p>toto toto toto</p><p>tata</p>",
|
||||
}, {
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
header: "<p> <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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
],
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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]",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in new issue
Block a user