Начальное наполнение

This commit is contained in:
parent 05249d6b97
commit 8109ee6c8c
190 changed files with 374677 additions and 1 deletions

100
README.md
View File

@ -1,2 +1,100 @@
# sale
Sales Management Made Easy
--------------------------
From quotes to invoices, in just a few clicks with the Odoo <a href="https://www.odoo.com/app/crm">Sales Management</a>.
Drive your sales operations from quotes to invoices with all the information
you need, easily accessible. Keep track of long term contracts, automate
invoicing and notify sales when they have things to do.
Create Professional Quotations
------------------------------
Create quotations in a matter of seconds. Send quotes by email or get a
professional PDF. Track quotations, and convert them to sales order in one
click.
Spend the extra time focusing on selling, not recording data.
Fully Integrated
----------------
The information your need, where you need it.
Don't lose time looking for customers, products or contracts related
information; they are all conveniently accessible when creating quotations.
Get access to stock availabilities in the different warehouses, to customer's
specific prices, to the history of preceeding offers for this prospect, etc.
Your Address Book
-----------------
So many features, so easy to use.
Assign tags to your prospects, manage
relationships between contacts and store all customer's preferences including
pricing, billing conditions, addresses, payment terms, etc.
Navigate through all the documents related to a customer with the powerfull
breadcrumb: quotations, invoices, emails, meetings.
Fully Integrated Invoicing
--------------------------
Whether you invoice based on time and materials, on delivery orders or fixed
price; Odoo supports all possible methods.
Get recurring invoices produced automatically, create advances in just a few
clicks, re-invoices expenses easily, etc.
Keep track of your contracts
----------------------------
Get rid of wasted paper and record all your contracts in the application.
Invoices are generated automatically based on your contract conditions. Your
account managers get alerts before contracts have to be renewed.
Communicate Efficiently With Customers
--------------------------------------
The chatter feature enables you to communicate faster and more efficiently with
your customer. This takes place directly on a quotation or sale order from
within Odoo or via email.
Get all the negotiations and discussions attached to the right document and
relevent managers notified on specific events.
Fully Extensible
----------------
By default, sales order are very simple, limited to a small number of features.
Don't be confused by features you don't need.
But you can activate options to fit your specific need: multi-warehouses, multi
unit of measures, manage customer specific prices with pricelists, control
margins on quotations, use different addresses for shipping and billing, etc.
Built-in Customer Relationship Management
-----------------------------------------
Activate the CRM application to manage your funnel of opportunities, attract
leads, log calls, schedule meetings and launch marketing campaigns.
Opportunities can be converted into quotations in just one click.
Drive Engagement with Gamification
----------------------------------
Align Sales Teams on clear targets. Define clear commission plans. Get real
time statistics on the performance of individual sales or channels. Motivate your
teams with challenges, rewards and leaderboards.
Have Clear Pricing Strategies
-----------------------------
Use pricelists to record special conditions for a specific customer or to
define prices for a segment of customers. Define promotions and have them
applied automatically for all your Sales Teams.

15
__init__.py Normal file
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from . import report
from . import wizard
from . import populate
def _synchronize_cron(env):
send_invoice_cron = env.ref('sale.send_invoice_cron', raise_if_not_found=False)
if send_invoice_cron:
config = env['ir.config_parameter'].get_param('sale.automatic_invoice', False)
send_invoice_cron.active = bool(config)

94
__manifest__.py Normal file
View File

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Sales',
'version': '1.2',
'category': 'Sales/Sales',
'summary': 'Sales internal machinery',
'description': """
This module contains all the common features of Sales Management and eCommerce.
""",
'depends': [
'sales_team',
'account_payment', # -> account, payment, portal
'utm',
],
'data': [
'security/ir.model.access.csv',
'security/res_groups.xml',
'security/ir_rules.xml',
'report/account_invoice_report_views.xml',
'report/ir_actions_report_templates.xml',
'report/ir_actions_report.xml',
'report/sale_report_views.xml',
'data/ir_cron.xml',
'data/ir_sequence_data.xml',
'data/mail_activity_type_data.xml',
'data/mail_message_subtype_data.xml',
'data/mail_template_data.xml',
'data/ir_config_parameter.xml', # Needs mail_template_data
'data/onboarding_data.xml',
'wizard/account_accrued_orders_wizard_views.xml',
'wizard/mass_cancel_orders_views.xml',
'wizard/payment_link_wizard_views.xml',
'wizard/res_config_settings_views.xml',
'wizard/sale_make_invoice_advance_views.xml',
'wizard/sale_order_cancel_views.xml',
'wizard/sale_order_discount_views.xml',
# Define sale order views before their references
'views/sale_order_views.xml',
'views/account_views.xml',
'views/crm_team_views.xml',
'views/mail_activity_views.xml',
'views/mail_activity_plan_views.xml',
'views/payment_views.xml',
'views/product_document_views.xml',
'views/product_packaging_views.xml',
'views/product_views.xml',
'views/res_partner_views.xml',
'views/sale_order_line_views.xml',
'views/sale_portal_templates.xml',
'views/utm_campaign_views.xml',
'views/sale_menus.xml', # Last because referencing actions defined in previous files
],
'demo': [
'data/product_demo.xml',
'data/sale_demo.xml',
],
'installable': True,
'assets': {
'web.assets_backend': [
'sale/static/src/scss/sale_onboarding.scss',
'sale/static/src/js/sale_progressbar_field.js',
'sale/static/src/js/tours/sale.js',
'sale/static/src/js/sale_product_field.js',
'sale/static/src/xml/**/*',
],
'web.assets_frontend': [
'sale/static/src/scss/sale_portal.scss',
'sale/static/src/js/sale_portal_sidebar.js',
'sale/static/src/js/sale_portal_prepayment.js',
'sale/static/src/js/sale_portal.js',
],
'web.assets_tests': [
'sale/static/tests/tours/**/*',
],
'web.qunit_suite_tests': [
'sale/static/tests/**/*',
('remove', 'sale/static/tests/tours/**/*')
],
'web.report_assets_common': [
'sale/static/src/scss/sale_report.scss',
],
},
'post_init_hook': '_synchronize_cron',
'license': 'LGPL-3',
}

4
controllers/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal

458
controllers/portal.py Normal file
View File

@ -0,0 +1,458 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import binascii
from odoo import fields, http, _
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.fields import Command
from odoo.http import request
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.controllers import portal as payment_portal
from odoo.addons.portal.controllers.mail import _message_post_helper
from odoo.addons.portal.controllers.portal import pager as portal_pager
class CustomerPortal(payment_portal.PaymentPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
partner = request.env.user.partner_id
SaleOrder = request.env['sale.order']
if 'quotation_count' in counters:
values['quotation_count'] = SaleOrder.search_count(self._prepare_quotations_domain(partner)) \
if SaleOrder.check_access_rights('read', raise_exception=False) else 0
if 'order_count' in counters:
values['order_count'] = SaleOrder.search_count(self._prepare_orders_domain(partner), limit=1) \
if SaleOrder.check_access_rights('read', raise_exception=False) else 0
return values
def _prepare_quotations_domain(self, partner):
return [
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', '=', 'sent')
]
def _prepare_orders_domain(self, partner):
return [
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', '=', 'sale'),
]
def _get_sale_searchbar_sortings(self):
return {
'date': {'label': _('Order Date'), 'order': 'date_order desc'},
'name': {'label': _('Reference'), 'order': 'name'},
'stage': {'label': _('Stage'), 'order': 'state'},
}
def _prepare_sale_portal_rendering_values(
self, page=1, date_begin=None, date_end=None, sortby=None, quotation_page=False, **kwargs
):
SaleOrder = request.env['sale.order']
if not sortby:
sortby = 'date'
partner = request.env.user.partner_id
values = self._prepare_portal_layout_values()
if quotation_page:
url = "/my/quotes"
domain = self._prepare_quotations_domain(partner)
else:
url = "/my/orders"
domain = self._prepare_orders_domain(partner)
searchbar_sortings = self._get_sale_searchbar_sortings()
sort_order = searchbar_sortings[sortby]['order']
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
pager_values = portal_pager(
url=url,
total=SaleOrder.search_count(domain),
page=page,
step=self._items_per_page,
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
)
orders = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager_values['offset'])
values.update({
'date': date_begin,
'quotations': orders.sudo() if quotation_page else SaleOrder,
'orders': orders.sudo() if not quotation_page else SaleOrder,
'page_name': 'quote' if quotation_page else 'order',
'pager': pager_values,
'default_url': url,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
})
return values
@http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_quotes(self, **kwargs):
values = self._prepare_sale_portal_rendering_values(quotation_page=True, **kwargs)
request.session['my_quotations_history'] = values['quotations'].ids[:100]
return request.render("sale.portal_my_quotations", values)
@http.route(['/my/orders', '/my/orders/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_orders(self, **kwargs):
values = self._prepare_sale_portal_rendering_values(quotation_page=False, **kwargs)
request.session['my_orders_history'] = values['orders'].ids[:100]
return request.render("sale.portal_my_orders", values)
@http.route(['/my/orders/<int:order_id>'], type='http', auth="public", website=True)
def portal_order_page(
self,
order_id,
report_type=None,
access_token=None,
message=False,
download=False,
downpayment=None,
**kw
):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
if report_type in ('html', 'pdf', 'text'):
return self._show_report(
model=order_sudo,
report_type=report_type,
report_ref='sale.action_report_saleorder',
download=download,
)
if request.env.user.share and access_token:
# If a public/portal user accesses the order with the access token
# Log a note on the chatter.
today = fields.Date.today().isoformat()
session_obj_date = request.session.get('view_quote_%s' % order_sudo.id)
if session_obj_date != today:
# store the date as a string in the session to allow serialization
request.session['view_quote_%s' % order_sudo.id] = today
# The "Quotation viewed by customer" log note is an information
# dedicated to the salesman and shouldn't be translated in the customer/website lgg
context = {'lang': order_sudo.user_id.partner_id.lang or order_sudo.company_id.partner_id.lang}
msg = _('Quotation viewed by customer %s', order_sudo.partner_id.name if request.env.user._is_public() else request.env.user.partner_id.name)
del context
_message_post_helper(
"sale.order",
order_sudo.id,
message=msg,
token=order_sudo.access_token,
message_type="notification",
subtype_xmlid="mail.mt_note",
partner_ids=order_sudo.user_id.sudo().partner_id.ids,
)
backend_url = f'/web#model={order_sudo._name}'\
f'&id={order_sudo.id}'\
f'&action={order_sudo._get_portal_return_action().id}'\
f'&view_type=form'
values = {
'sale_order': order_sudo,
'product_documents': order_sudo._get_product_documents(),
'message': message,
'report_type': 'html',
'backend_url': backend_url,
'res_company': order_sudo.company_id, # Used to display correct company logo
}
# Payment values
if order_sudo._has_to_be_paid():
values.update(
self._get_payment_values(
order_sudo,
downpayment=downpayment == 'true' if downpayment is not None else order_sudo.prepayment_percent < 1.0
)
)
if order_sudo.state in ('draft', 'sent', 'cancel'):
history_session_key = 'my_quotations_history'
else:
history_session_key = 'my_orders_history'
values = self._get_page_view_values(
order_sudo, access_token, values, history_session_key, False)
return request.render('sale.sale_order_portal_template', values)
def _get_payment_values(self, order_sudo, downpayment=False, **kwargs):
""" Return the payment-specific QWeb context values.
:param sale.order order_sudo: The sales order being paid.
:param bool downpayment: Whether the current payment is a downpayment.
:param dict kwargs: Locally unused data passed to `_get_compatible_providers` and
`_get_available_tokens`.
:return: The payment-specific values.
:rtype: dict
"""
logged_in = not request.env.user._is_public()
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_id
company = order_sudo.company_id
if downpayment:
amount = order_sudo._get_prepayment_required_amount()
else:
amount = order_sudo.amount_total - order_sudo.amount_paid
currency = order_sudo.currency_id
# Select all the payment methods and tokens that match the payment context.
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
company.id,
partner_sudo.id,
amount,
currency_id=currency.id,
sale_order_id=order_sudo.id,
**kwargs,
) # In sudo mode to read the fields of providers and partner (if logged out).
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
providers_sudo.ids,
partner_sudo.id,
currency_id=currency.id,
) # In sudo mode to read the fields of providers.
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
providers_sudo.ids, partner_sudo.id, **kwargs
) # In sudo mode to read the partner's tokens (if logged out) and provider fields.
# Make sure that the partner's company matches the invoice's company.
company_mismatch = not payment_portal.PaymentPortal._can_partner_pay_in_company(
partner_sudo, company
)
portal_page_values = {
'company_mismatch': company_mismatch,
'expected_company': company,
}
payment_form_values = {
'show_tokenize_input_mapping': PaymentPortal._compute_show_tokenize_input_mapping(
providers_sudo, sale_order_id=order_sudo.id
),
}
payment_context = {
'amount': amount,
'currency': currency,
'partner_id': partner_sudo.id,
'providers_sudo': providers_sudo,
'payment_methods_sudo': payment_methods_sudo,
'tokens_sudo': tokens_sudo,
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
'landing_route': order_sudo.get_portal_url(),
'access_token': order_sudo._portal_ensure_token(),
}
return {
**portal_page_values,
**payment_form_values,
**payment_context,
**self._get_extra_payment_form_values(**kwargs),
}
@http.route(['/my/orders/<int:order_id>/accept'], type='json', auth="public", website=True)
def portal_quote_accept(self, order_id, access_token=None, name=None, signature=None):
# get from query string if not on json param
access_token = access_token or request.httprequest.args.get('access_token')
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return {'error': _('Invalid order.')}
if not order_sudo._has_to_be_signed():
return {'error': _('The order is not in a state requiring customer signature.')}
if not signature:
return {'error': _('Signature is missing.')}
try:
order_sudo.write({
'signed_by': name,
'signed_on': fields.Datetime.now(),
'signature': signature,
})
request.env.cr.commit()
except (TypeError, binascii.Error) as e:
return {'error': _('Invalid signature data.')}
if not order_sudo._has_to_be_paid():
order_sudo.action_confirm()
order_sudo._send_order_confirmation_mail()
pdf = request.env['ir.actions.report'].sudo()._render_qweb_pdf('sale.action_report_saleorder', [order_sudo.id])[0]
_message_post_helper(
'sale.order',
order_sudo.id,
_('Order signed by %s', name),
attachments=[('%s.pdf' % order_sudo.name, pdf)],
token=access_token,
)
query_string = '&message=sign_ok'
if order_sudo._has_to_be_paid():
query_string += '#allow_payment=yes'
return {
'force_refresh': True,
'redirect_url': order_sudo.get_portal_url(query_string=query_string),
}
@http.route(['/my/orders/<int:order_id>/decline'], type='http', auth="public", methods=['POST'], website=True)
def portal_quote_decline(self, order_id, access_token=None, decline_message=None, **kwargs):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
if order_sudo._has_to_be_signed() and decline_message:
order_sudo._action_cancel()
_message_post_helper(
'sale.order',
order_sudo.id,
decline_message,
token=access_token,
)
redirect_url = order_sudo.get_portal_url()
else:
redirect_url = order_sudo.get_portal_url(query_string="&message=cant_reject")
return request.redirect(redirect_url)
@http.route('/my/orders/<int:order_id>/document/<int:document_id>', type='http', auth='public')
def portal_quote_document(self, order_id, document_id, access_token):
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
document = request.env['product.document'].browse(document_id).sudo().exists()
if not document or not document.active:
return request.redirect('/my')
if document not in order_sudo._get_product_documents():
return request.redirect('/my')
return request.env['ir.binary']._get_stream_from(
document.ir_attachment_id,
).get_response(as_attachment=True)
class PaymentPortal(payment_portal.PaymentPortal):
@http.route('/my/orders/<int:order_id>/transaction', type='json', auth='public')
def portal_order_transaction(self, order_id, access_token, **kwargs):
""" Create a draft transaction and return its processing values.
:param int order_id: The sales order to pay, as a `sale.order` id
:param str access_token: The access token used to authenticate the request
:param dict kwargs: Locally unused data passed to `_create_transaction`
:return: The mandatory values for the processing of the transaction
:rtype: dict
:raise: ValidationError if the invoice id or the access token is invalid
"""
# Check the order id and the access token
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token)
except MissingError as error:
raise error
except AccessError:
raise ValidationError(_("The access token is invalid."))
logged_in = not request.env.user._is_public()
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_invoice_id
self._validate_transaction_kwargs(kwargs)
kwargs.update({
'partner_id': partner_sudo.id,
'currency_id': order_sudo.currency_id.id,
'sale_order_id': order_id, # Include the SO to allow Subscriptions tokenizing the tx
})
tx_sudo = self._create_transaction(
custom_create_values={'sale_order_ids': [Command.set([order_id])]}, **kwargs,
)
return tx_sudo._get_processing_values()
# Payment overrides
@http.route()
def payment_pay(self, *args, amount=None, sale_order_id=None, access_token=None, **kwargs):
""" Override of `payment` to replace the missing transaction values by that of the sales
order.
:param str amount: The (possibly partial) amount to pay used to check the access token
:param str sale_order_id: The sale order for which a payment id made, as a `sale.order` id
:param str access_token: The access token used to authenticate the partner
:return: The result of the parent method
:rtype: str
:raise: ValidationError if the order id is invalid
"""
# Cast numeric parameters as int or float and void them if their str value is malformed
amount = self._cast_as_float(amount)
sale_order_id = self._cast_as_int(sale_order_id)
if sale_order_id:
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id).exists()
if not order_sudo:
raise ValidationError(_("The provided parameters are invalid."))
# Check the access token against the order values. Done after fetching the order as we
# need the order fields to check the access token.
if not payment_utils.check_access_token(
access_token, order_sudo.partner_invoice_id.id, amount, order_sudo.currency_id.id
):
raise ValidationError(_("The provided parameters are invalid."))
kwargs.update({
# To display on the payment form; will be later overwritten when creating the tx.
'reference': order_sudo.name,
# To fix the currency if incorrect and avoid mismatches when creating the tx.
'currency_id': order_sudo.currency_id.id,
# To fix the partner if incorrect and avoid mismatches when creating the tx.
'partner_id': order_sudo.partner_invoice_id.id,
'company_id': order_sudo.company_id.id,
'sale_order_id': sale_order_id,
})
return super().payment_pay(*args, amount=amount, access_token=access_token, **kwargs)
def _get_extra_payment_form_values(self, sale_order_id=None, access_token=None, **kwargs):
""" Override of `payment` to reroute the payment flow to the portal view of the sales order.
:param str sale_order_id: The sale order for which a payment is made, as a `sale.order` id.
:param str access_token: The portal or payment access token, respectively if we are in a
portal or payment link flow.
:return: The extended rendering context values.
:rtype: dict
"""
form_values = super()._get_extra_payment_form_values(
sale_order_id=sale_order_id, access_token=access_token, **kwargs
)
if sale_order_id:
sale_order_id = self._cast_as_int(sale_order_id)
try: # Check document access against what could be a portal access token.
order_sudo = self._document_check_access('sale.order', sale_order_id, access_token)
except AccessError: # It is a payment access token computed on the payment context.
if not payment_utils.check_access_token(
access_token,
kwargs.get('partner_id'),
kwargs.get('amount'),
kwargs.get('currency_id'),
):
raise
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id)
# Interrupt the payment flow if the sales order has been canceled.
if order_sudo.state == 'cancel':
form_values['amount'] = 0.0
# Reroute the next steps of the payment flow to the portal view of the sales order.
form_values.update({
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
'landing_route': order_sudo.get_portal_url(),
'access_token': order_sudo.access_token,
})
return form_values

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="default_confirmation_template" model="ir.config_parameter">
<field name="key">sale.default_confirmation_template</field>
<field name="value" ref="sale.mail_template_sale_confirmation"/>
</record>
<record id="default_invoice_email_template" model="ir.config_parameter">
<field name="key">sale.default_invoice_email_template</field>
<field name="value" ref="account.email_template_edi_invoice"/>
</record>
</odoo>

16
data/ir_cron.xml Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="send_invoice_cron" model="ir.cron">
<field name="name">automatic invoicing: send ready invoice</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<field name="state">code</field>
<field name="code">model._cron_send_invoice()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">False</field>
</record>
</odoo>

12
data/ir_sequence_data.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="seq_sale_order" model="ir.sequence">
<field name="name">Sales Order</field>
<field name="code">sale.order</field>
<field name="prefix">S</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Activities -->
<record id="mail_act_sale_upsell" model="mail.activity.type">
<field name="name">Order Upsell</field>
<field name="icon">fa-line-chart</field>
<field name="res_model">sale.order</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sale-related subtypes for messaging / Chatter -->
<record id="mt_order_sent" model="mail.message.subtype">
<field name="name">Quotation sent</field>
<field name="res_model">sale.order</field>
<field name="default" eval="False"/>
<field name="description">Quotation sent</field>
</record>
<record id="mt_order_confirmed" model="mail.message.subtype">
<field name="name">Sales Order Confirmed</field>
<field name="res_model">sale.order</field>
<field name="default" eval="False"/>
<field name="description">Quotation confirmed</field>
</record>
<!-- Salesteam-related subtypes for messaging / Chatter -->
<record id="mt_salesteam_order_sent" model="mail.message.subtype">
<field name="name">Quotation sent</field>
<field name="sequence">20</field>
<field name="res_model">crm.team</field>
<field name="default" eval="True"/>
<field name="parent_id" ref="sale.mt_order_sent"/>
<field name="relation_field">team_id</field>
</record>
<record id="mt_salesteam_order_confirmed" model="mail.message.subtype">
<field name="name">Sales Order Confirmed</field>
<field name="sequence">21</field>
<field name="res_model">crm.team</field>
<field name="default" eval="True"/>
<field name="parent_id" ref="sale.mt_order_confirmed"/>
<field name="relation_field">team_id</field>
</record>
<record id="mt_salesteam_invoice_created" model="mail.message.subtype">
<field name="name">Invoice Created</field>
<field name="sequence">22</field>
<field name="res_model">crm.team</field>
<field name="parent_id" ref="account.mt_invoice_created"/>
<field name="relation_field">team_id</field>
</record>
<record id="mt_salesteam_invoice_confirmed" model="mail.message.subtype">
<field name="name">Invoice Confirmed</field>
<field name="sequence">23</field>
<field name="res_model">crm.team</field>
<field name="parent_id" ref="account.mt_invoice_validated"/>
<field name="relation_field">team_id</field>
</record>
</data>
</odoo>

330
data/mail_template_data.xml Normal file
View File

@ -0,0 +1,330 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="email_template_edi_sale" model="mail.template">
<field name="name">Sales: Send Quotation</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">{{ object.company_id.name }} {{ object.state in ('draft', 'sent') and (ctx.get('proforma') and 'Proforma' or 'Quotation') or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Used by salespeople when they send quotations or proforma to prospects</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
<t t-set="doc_name" t-value="'quotation' if object.state in ('draft', 'sent') else 'order'"/>
Hello,
<br/><br/>
Your
<t t-if="ctx.get('proforma')">
Pro forma invoice for <t t-out="doc_name or ''">quotation</t> <span style="font-weight: bold;" t-out="object.name or ''">S00052</span>
<t t-if="object.origin">
(with reference: <t t-out="object.origin or ''"></t> )
</t>
amounting in <span style="font-weight: bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span> is available.
</t>
<t t-else="">
<t t-out="doc_name or ''">quotation</t> <span style="font-weight: bold;" t-out="object.name or ''"></span>
<t t-if="object.origin">
(with reference: <t t-out="object.origin or ''">S00052</t> )
</t>
amounting in <span style="font-weight: bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span> is ready for review.
</t>
<br/><br/>
Do not hesitate to contact us if you have any questions.
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
</t>
<br/><br/>
</p>
</div>
</field>
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="mail_template_sale_confirmation" model="mail.template">
<field name="name">Sales: Order Confirmation</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">{{ object.company_id.name }} {{ (object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Sent to customers on order confirmation</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 12px;">
Hello,
<br/><br/>
<t t-set="tx_sudo" t-value="object.get_portal_last_transaction()"/>
Your order <span style="font-weight:bold;" t-out="object.name or ''">S00049</span> amounting in <span style="font-weight:bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span>
<t t-if="object.state == 'sale' or (tx_sudo and tx_sudo.state in ('done', 'authorized'))">
has been confirmed.<br/>
Thank you for your trust!
</t>
<t t-elif="tx_sudo and tx_sudo.state == 'pending'">
is pending. It will be confirmed when the payment is received.
<t t-if="object.reference">
Your payment reference is <span style="font-weight:bold;" t-out="object.reference or ''"></span>.
</t>
</t>
<br/><br/>
Do not hesitate to contact us if you have any questions.
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
</t>
<br/><br/>
</p>
<t t-if="hasattr(object, 'website_id') and object.website_id">
<div style="margin: 0px; padding: 0px;">
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
<tr style="border-bottom: 2px solid #dee2e6;">
<td style="width: 150px;"><span style="font-weight:bold;">Products</span></td>
<td></td>
<td width="15%" align="center"><span style="font-weight:bold;">Quantity</span></td>
<td width="20%" align="right">
<span style="font-weight:bold;">
<t t-if="object.website_id.show_line_subtotals_tax_selection == 'tax_excluded'">
VAT Excl.
</t>
<t t-else="">
VAT Incl.
</t>
</span>
</td>
</tr>
</table>
<t t-foreach="object.order_line" t-as="line">
<t t-if="(not hasattr(line, 'is_delivery') or not line.is_delivery) and line.display_type in ['line_section', 'line_note']">
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
<t t-set="loop_cycle_number" t-value="loop_cycle_number or 0" />
<tr t-att-style="'background-color: #f2f2f2' if loop_cycle_number % 2 == 0 else 'background-color: #ffffff'">
<t t-set="loop_cycle_number" t-value="loop_cycle_number + 1" />
<td colspan="4">
<t t-if="line.display_type == 'line_section'">
<span style="font-weight:bold;" t-out="line.name or ''">Taking care of Trees Course</span>
</t>
<t t-elif="line.display_type == 'line_note'">
<i t-out="line.name or ''">Taking care of Trees Course</i>
</t>
</td>
</tr>
</table>
</t>
<t t-elif="(not hasattr(line, 'is_delivery') or not line.is_delivery)">
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
<t t-set="loop_cycle_number" t-value="loop_cycle_number or 0" />
<tr t-att-style="'background-color: #f2f2f2' if loop_cycle_number % 2 == 0 else 'background-color: #ffffff'">
<t t-set="loop_cycle_number" t-value="loop_cycle_number + 1" />
<td style="width: 150px;">
<img t-attf-src="/web/image/product.product/{{ line.product_id.id }}/image_128" style="width: 64px; height: 64px; object-fit: contain;" alt="Product image"></img>
</td>
<td align="left" t-out="line.product_id.name or ''"> Taking care of Trees Course</td>
<td width="15%" align="center" t-out="line.product_uom_qty or ''">1</td>
<td width="20%" align="right"><span style="font-weight:bold;">
<t t-if="object.website_id.show_line_subtotals_tax_selection == 'tax_excluded'">
<t t-out="format_amount(line.price_reduce_taxexcl, object.currency_id) or ''">$ 10.00</t>
</t>
<t t-else="">
<t t-out="format_amount(line.price_reduce_taxinc, object.currency_id) or ''">$ 10.00</t>
</t>
</span></td>
</tr>
</table>
</t>
</t>
</div>
<div style="margin: 0px; padding: 0px;" t-if="hasattr(object, 'carrier_id') and object.carrier_id">
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
<tr>
<td style="width: 60%"/>
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">Delivery:</span></td>
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_delivery, object.currency_id) or ''">$ 0.00</td>
</tr>
<tr>
<td style="width: 60%"/>
<td style="width: 30%;" align="right"><span style="font-weight:bold;">SubTotal:</span></td>
<td style="width: 10%;" align="right" t-out="format_amount(object.amount_untaxed, object.currency_id) or ''">$ 10.00</td>
</tr>
</table>
</div>
<div style="margin: 0px; padding: 0px;" t-else="">
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
<tr>
<td style="width: 60%"/>
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">SubTotal:</span></td>
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_untaxed, object.currency_id) or ''">$ 10.00</td>
</tr>
</table>
</div>
<div style="margin: 0px; padding: 0px;">
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
<tr>
<td style="width: 60%"/>
<td style="width: 30%;" align="right"><span style="font-weight:bold;">Taxes:</span></td>
<td style="width: 10%;" align="right" t-out="format_amount(object.amount_tax, object.currency_id) or ''">$ 0.00</td>
</tr>
<tr>
<td style="width: 60%"/>
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">Total:</span></td>
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</td>
</tr>
</table>
</div>
<div t-if="object.partner_invoice_id" style="margin: 0px; padding: 0px;">
<table width="100%" style="color: #454748; font-size: 12px;">
<tr>
<td style="padding-top: 10px;">
<span style="font-weight:bold;">Bill to:</span>
<t t-out="object.partner_invoice_id.street or ''">1201 S Figueroa St</t>
<t t-out="object.partner_invoice_id.city or ''">Los Angeles</t>
<t t-out="object.partner_invoice_id.state_id.name or ''">California</t>
<t t-out="object.partner_invoice_id.zip or ''">90015</t>
<t t-out="object.partner_invoice_id.country_id.name or ''">United States</t>
</td>
</tr>
<tr>
<td>
<span style="font-weight:bold;">Payment Method:</span>
<t t-if="tx_sudo.token_id">
<t t-out="tx_sudo.token_id.display_name or ''"></t>
</t>
<t t-else="">
<t t-out="tx_sudo.provider_id.sudo().name or ''"></t>
</t>
(<t t-out="format_amount(tx_sudo.amount, object.currency_id) or ''">$ 10.00</t>)
</td>
</tr>
</table>
</div>
<div t-if="object.partner_shipping_id and not object.only_services" style="margin: 0px; padding: 0px;">
<table width="100%" style="color: #454748; font-size: 12px;">
<tr>
<td>
<br/>
<span style="font-weight:bold;">Ship to:</span>
<t t-out="object.partner_shipping_id.street or ''">1201 S Figueroa St</t>
<t t-out="object.partner_shipping_id.city or ''">Los Angeles</t>
<t t-out="object.partner_shipping_id.state_id.name or ''">California</t>
<t t-out="object.partner_shipping_id.zip or ''">90015</t>
<t t-out="object.partner_shipping_id.country_id.name or ''">United States</t>
</td>
</tr>
</table>
<table t-if="hasattr(object, 'carrier_id') and object.carrier_id" width="100%" style="color: #454748; font-size: 12px;">
<tr>
<td>
<span style="font-weight:bold;">Shipping Method:</span>
<t t-out="object.carrier_id.name or ''"></t>
<t t-if="object.amount_delivery == 0.0">
(Free)
</t>
<t t-else="">
(<t t-out="format_amount(object.amount_delivery, object.currency_id) or ''">$ 10.00</t>)
</t>
</td>
</tr>
<tr t-if="object.carrier_id.carrier_description">
<td>
<strong>Shipping Description:</strong>
<t t-out="object.carrier_id.carrier_description"/>
</td>
</tr>
</table>
</div>
</t>
</div></field>
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="mail_template_sale_payment_executed" model="mail.template">
<field name="name">Sales: Payment Done</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">{{ object.company_id.name }} {{ (object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Sent to customers when a payment is received but doesn't immediately confirm their order</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 12px;">
<t t-set="transaction_sudo" t-value="object.get_portal_last_transaction()"/>
Hello,
<br/><br/>
A payment with reference
<span style="font-weight:bold;" t-out="transaction_sudo.reference or ''">SOOO49</span>
amounting
<span style="font-weight:bold;" t-out="format_amount(transaction_sudo.amount, object.currency_id) or ''">$ 10.00</span>
for your order
<span style="font-weight:bold;" t-out="object.name or ''">S00049</span>
<t t-if="transaction_sudo and transaction_sudo.state == 'pending'">
is pending.
<br/>
<t t-if="object.currency_id.compare_amounts(object.amount_paid + transaction_sudo.amount, object.amount_total) >= 0 and object.state in ('draft', 'sent')">
Your order will be confirmed once the payment is confirmed.
</t>
<t t-else="">
Once confirmed,
<span style="font-weight:bold;" t-out="format_amount(object.amount_total - object.amount_paid - transaction_sudo.amount, object.currency_id) or ''">$ 10.00</span>
will remain to be paid.
</t>
</t>
<t t-else="">
has been confirmed.
<t t-if="object.currency_id.compare_amounts(object.amount_paid, object.amount_total) &lt; 0">
<br/>
<span style="font-weight:bold;" t-out="format_amount(object.amount_total - object.amount_paid, object.currency_id) or ''">$ 10.00</span>
remains to be paid.
</t>
</t>
<br/><br/>
Thank you for your trust!
<br/>
Do not hesitate to contact us if you have any questions.
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
</t>
<br/><br/>
</p>
</div>
</field>
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="sale.mail_template_sale_cancellation" model="mail.template">
<field name="name">Sales: Order Cancellation</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">{{ object.company_id.name }} {{ object.type_name }} Cancelled (Ref {{ object.name or 'n/a' }})</field>
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Sent automatically to customers when you cancel an order</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
<t t-set="doc_name" t-value="object.type_name"/>
Dear <t t-out="object.partner_id.name or ''">user</t>,
<br/><br/>
Please be advised that your
<t t-out="doc_name or ''">quotation</t> <strong t-out="object.name or ''">S00052</strong>
<t t-if="object.origin">
(with reference: <t t-out="object.origin or ''">S00052</t> )
</t>
has been cancelled. Therefore, you should not be charged further for this order.
If any refund is necessary, this will be executed at best convenience.
<br/><br/>
Do not hesitate to contact us if you have any questions.
<br/>
</p>
</div>
</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

41
data/onboarding_data.xml Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sale Quotation - ONBOARDING STEPS -->
<record id="onboarding_onboarding_step_sale_order_confirmation" model="onboarding.onboarding.step">
<field name="title">Order Confirmation</field>
<field name="description">Choose between electronic signatures or online payments.</field>
<field name="button_text">Set payments</field>
<field name="panel_step_open_action_name">action_open_step_sale_order_confirmation</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_default.png"></field>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding Order Confirmation</field>
<field name="sequence">6</field>
</record>
<record id="onboarding_onboarding_step_sample_quotation" model="onboarding.onboarding.step">
<field name="title">Sample Quotation</field>
<field name="description">Send a quotation to test the customer portal.</field>
<field name="button_text">Send sample</field>
<field name="panel_step_open_action_name">action_open_step_sample_quotation</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_sample-quotation.png"></field>
<field name="step_image_filename">onboarding_sample-quotation.png</field>
<field name="step_image_alt">Onboarding Sample Quotation</field>
<field name="sequence">7</field>
</record>
<!-- Sale Quotation - ONBOARDING PANEL -->
<record id="onboarding_onboarding_sale_quotation" model="onboarding.onboarding">
<field name="name">Sale Quotation Onboarding</field>
<field name="step_ids" eval="[
Command.link(ref('sale.onboarding_onboarding_step_sale_order_confirmation')),
Command.link(ref('sale.onboarding_onboarding_step_sample_quotation')),
Command.link(ref('account.onboarding_onboarding_step_company_data')),
Command.link(ref('account.onboarding_onboarding_step_base_document_layout'))
]"/>
<field name="route_name">sale_quotation</field>
<field name="panel_close_action_name">action_close_panel_sale_quotation</field>
</record>
</data>
</odoo>

201
data/product_demo.xml Normal file
View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="product.consu_delivery_01" model="product.product">
<field name="invoice_policy">order</field>
</record>
<record id="product.consu_delivery_02" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.consu_delivery_03" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_order_01" model="product.product">
<field name="invoice_policy">order</field>
</record>
<record id="product.product_delivery_01" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_27" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_25" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_24" model="product.product">
<field name="invoice_policy">order</field>
</record>
<record id="product.product_product_22" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_20" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_16" model="product.product">
<field name="invoice_policy">order</field>
</record>
<record id="product.product_product_13" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_12" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_11b" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_11" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_10" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_9" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_8" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_7" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_6" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_5" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_4c" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_4b" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_4" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_3" model="product.product">
<field name="invoice_policy">delivery</field>
<field name="expense_policy">cost</field>
</record>
<record id="product.product_product_2" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<record id="product.product_product_1" model="product.product">
<field name="invoice_policy">delivery</field>
</record>
<!-- Expensable products -->
<record id="product.expense_product" model="product.product">
<field name="invoice_policy">order</field>
<field name="expense_policy">sales_price</field>
</record>
<record id="product.expense_hotel" model="product.product">
<field name="invoice_policy">delivery</field>
<field name="expense_policy">cost</field>
</record>
<record id="product.product_attribute_2" model="product.attribute">
<field name="display_type">color</field>
</record>
<record id="product.product_attribute_3" model="product.attribute">
<field name="display_type">select</field>
</record>
<record id="product.product_attribute_value_3" model="product.attribute.value">
<field name="html_color">#FFFFFF</field>
</record>
<record id="product.product_attribute_value_4" model="product.attribute.value">
<field name="html_color">#000000</field>
</record>
<record id="product_attribute_value_7" model="product.attribute.value">
<field name="name">Custom</field>
<field name="attribute_id" ref="product.product_attribute_1"/>
<field name="is_custom">True</field>
<field name="sequence">3</field>
</record>
<record id="product.product_4_attribute_1_product_template_attribute_line" model="product.template.attribute.line">
<field name="value_ids" eval="[(4,ref('product_attribute_value_7'))]"/>
</record>
<!--
Handle automatically created product.template.attribute.value.
Check "product.product_4_attribute_1_value_2" for more information about this
-->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'sale.product_4_attribute_1_value_3',
'record': obj().env.ref('product.product_4_attribute_1_product_template_attribute_line').product_template_value_ids[2],
'noupdate': True,
}]"/>
</function>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'sale.product_product_4e',
'record': obj().env.ref('product.product_product_4_product_template')._get_variant_for_combination(obj().env.ref('sale.product_4_attribute_1_value_3') + obj().env.ref('product.product_4_attribute_2_value_1')),
'noupdate': True,
}, {
'xml_id': 'sale.product_product_4f',
'record': obj().env.ref('product.product_product_4_product_template')._get_variant_for_combination(obj().env.ref('sale.product_4_attribute_1_value_3') + obj().env.ref('product.product_4_attribute_2_value_2')),
'noupdate': True,
},]"/>
</function>
<record id="product_product_4e" model="product.product">
<field name="default_code">DESK0005</field>
<field name="weight">0.01</field>
</record>
<record id="product_product_4f" model="product.product">
<field name="default_code">DESK0006</field>
<field name="weight">0.01</field>
</record>
<record id="advance_product_0" model="product.product">
<field name="name">Deposit</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="type">service</field>
<field name="list_price">150.0</field>
<field name="invoice_policy">order</field>
<field name="standard_price">100.0</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="company_id" eval="[]"/>
<field name="image_1920" type="base64" file="sale/static/img/advance_product_0-image.jpg"/>
<field name="taxes_id" eval="[]"/>
<field name="supplier_taxes_id" eval="[]"/>
</record>
</odoo>

712
data/sale_demo.xml Normal file
View File

@ -0,0 +1,712 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- We want to activate pay and sign by default for easier demoing. -->
<record id="base.main_company" model="res.company">
<field name="portal_confirmation_pay" eval="True"/>
</record>
<record id="base.user_demo" model="res.users">
<field eval="[(4, ref('sales_team.group_sale_salesman'))]" name="groups_id"/>
</record>
<record model="crm.team" id="sales_team.team_sales_department">
<field name="invoiced_target">250000</field>
</record>
<record model="crm.team" id="sales_team.crm_team_1">
<field name="invoiced_target">40000</field>
</record>
<record id="utm_source_sale_order_0" model="utm.source">
<field name="name">Sale Promotion 1</field>
</record>
<record id="sale_order_1" model="sale.order">
<field name="partner_id" ref="base.res_partner_2"/>
<field name="partner_invoice_id" ref="base.res_partner_2"/>
<field name="partner_shipping_id" ref="base.res_partner_2"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="sale_order_line_1" model="sale.order.line">
<field name="order_id" ref="sale_order_1"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">295.00</field>
</record>
<record id="sale_order_line_2" model="sale.order.line">
<field name="order_id" ref="sale_order_1"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="product_uom_qty">5</field>
<field name="price_unit">145.00</field>
</record>
<record id="sale_order_line_3" model="sale.order.line">
<field name="order_id" ref="sale_order_1"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">65.00</field>
</record>
<record id="sale_order_2" model="sale.order">
<field name="partner_id" ref="base.res_partner_4"/>
<field name="partner_invoice_id" ref="base.res_partner_address_13"/>
<field name="partner_shipping_id" ref="base.res_partner_address_13"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor7'))]"/>
</record>
<record id="sale_order_line_4" model="sale.order.line">
<field name="order_id" ref="sale_order_2"/>
<field name="product_id" ref="product.product_product_1"/>
<field name="product_uom_qty">24</field>
<field name="price_unit">75.00</field>
</record>
<record id="sale_order_line_5" model="sale.order.line">
<field name="order_id" ref="sale_order_2"/>
<field name="product_id" ref="product.product_product_2"/>
<field name="product_uom_qty">30</field>
<field name="price_unit">38.25</field>
</record>
<record id="sale_order_3" model="sale.order">
<field name="partner_id" ref="base.res_partner_4"/>
<field name="partner_invoice_id" ref="base.res_partner_4"/>
<field name="partner_shipping_id" ref="base.res_partner_4"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor1')), (4, ref('sales_team.categ_oppor2'))]"/>
</record>
<record id="sale_order_line_6" model="sale.order.line">
<field name="order_id" ref="sale_order_3"/>
<field name="product_id" ref="product.product_product_1"/>
<field name="product_uom_qty">10</field>
<field name="price_unit">30.75</field>
</record>
<record id="sale_order_line_7" model="sale.order.line">
<field name="order_id" ref="sale_order_3"/>
<field name="product_id" ref="product.product_delivery_01"/>
</record>
<record id="sale_order_4" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
</record>
<record id="sale_order_line_8" model="sale.order.line">
<field name="order_id" ref="sale_order_4"/>
<field name="product_id" ref="product.product_product_1"/>
<field name="product_uom_qty">16</field>
<field name="price_unit">75.00</field>
</record>
<record id="sale_order_line_9" model="sale.order.line">
<field name="order_id" ref="sale_order_4"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="product_uom_qty">10</field>
<field name="price_unit">45.00</field>
</record>
<record id="sale_order_line_10" model="sale.order.line">
<field name="order_id" ref="sale_order_4"/>
<field name="product_id" ref="product.consu_delivery_02"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">150.00</field>
</record>
<record id="sale_order_line_11" model="sale.order.line">
<field name="order_id" ref="sale_order_4"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="product_uom_qty">2</field>
</record>
<record id="sale_order_5" model="sale.order">
<field name="partner_id" ref="base.res_partner_2"/>
<field name="partner_invoice_id" ref="base.res_partner_2"/>
<field name="partner_shipping_id" ref="base.res_partner_2"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="sale_order_line_12" model="sale.order.line">
<field name="order_id" ref="sale_order_5"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="price_unit">405.00</field>
</record>
<record id="sale_order_6" model="sale.order">
<field name="partner_id" ref="base.res_partner_18"/>
<field name="partner_invoice_id" ref="base.res_partner_18"/>
<field name="partner_shipping_id" ref="base.res_partner_18"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor6'))]"/>
</record>
<record id="sale_order_line_15" model="sale.order.line">
<field name="order_id" ref="sale_order_6"/>
<field name="product_id" ref="product.product_product_4"/>
<field name="price_unit">750.00</field>
</record>
<record id="sale_order_7" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_11"/>
<field name="partner_shipping_id" ref="base.res_partner_address_11"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor4'))]"/>
</record>
<record id="sale_order_line_16" model="sale.order.line">
<field name="order_id" ref="sale_order_7"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">5</field>
<field name="price_unit">295.00</field>
</record>
<record id="sale_order_line_17" model="sale.order.line">
<field name="order_id" ref="sale_order_7"/>
<field name="product_id" ref="product.consu_delivery_01"/>
<field name="price_unit">173.00</field>
</record>
<record id="sale_order_line_18" model="sale.order.line">
<field name="order_id" ref="sale_order_7"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="price_unit">40.00</field>
</record>
<record id="sale_order_line_19" model="sale.order.line">
<field name="order_id" ref="sale_order_7"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="price_unit">18.00</field>
</record>
<record id="sale_order_8" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
</record>
<record id="sale_order_line_20" model="sale.order.line">
<field name="order_id" ref="sale_order_8"/>
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">110.50</field>
</record>
<record id="sale_order_line_21" model="sale.order.line">
<field name="order_id" ref="sale_order_8"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">120.50</field>
</record>
<!-- additional demo data for pretty graphs in sales dashboard -->
<record id="sale_order_9" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_22" model="sale.order.line">
<field name="order_id" ref="sale_order_9"/>
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">97.50</field>
</record>
<record id="sale_order_line_23" model="sale.order.line">
<field name="order_id" ref="sale_order_9"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_10" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=14)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor3'))]"/>
</record>
<record id="sale_order_line_24" model="sale.order.line">
<field name="order_id" ref="sale_order_10"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">255.00</field>
</record>
<record id="sale_order_line_25" model="sale.order.line">
<field name="order_id" ref="sale_order_10"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_11" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=21)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_26" model="sale.order.line">
<field name="order_id" ref="sale_order_11"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">245.00</field>
</record>
<record id="sale_order_line_27" model="sale.order.line">
<field name="order_id" ref="sale_order_11"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_12" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=28)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor1'))]"/>
</record>
<record id="sale_order_line_28" model="sale.order.line">
<field name="order_id" ref="sale_order_12"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="price_unit">315.00</field>
</record>
<record id="sale_order_line_29" model="sale.order.line">
<field name="order_id" ref="sale_order_12"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_13" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=35)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_30" model="sale.order.line">
<field name="order_id" ref="sale_order_13"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="price_unit">295.00</field>
</record>
<record id="sale_order_line_31" model="sale.order.line">
<field name="order_id" ref="sale_order_13"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_14" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_32" model="sale.order.line">
<field name="order_id" ref="sale_order_14"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">4</field>
<field name="price_unit">275.00</field>
</record>
<record id="sale_order_line_33" model="sale.order.line">
<field name="order_id" ref="sale_order_14"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">4</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_15" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=14)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_34" model="sale.order.line">
<field name="order_id" ref="sale_order_15"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">4</field>
<field name="price_unit">295.00</field>
</record>
<record id="sale_order_line_35" model="sale.order.line">
<field name="order_id" ref="sale_order_15"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_16" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=21)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_36" model="sale.order.line">
<field name="order_id" ref="sale_order_16"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">275.00</field>
</record>
<record id="sale_order_line_37" model="sale.order.line">
<field name="order_id" ref="sale_order_16"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_17" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=28)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_38" model="sale.order.line">
<field name="order_id" ref="sale_order_17"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">355.00</field>
</record>
<record id="sale_order_line_39" model="sale.order.line">
<field name="order_id" ref="sale_order_17"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">120.50</field>
</record>
<record id="sale_order_18" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
<field name="user_id" ref="base.user_demo"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="date_order" eval="(datetime.now()-relativedelta(days=35)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="sale_order_line_40" model="sale.order.line">
<field name="order_id" ref="sale_order_18"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">295.00</field>
</record>
<record id="sale_order_line_41" model="sale.order.line">
<field name="order_id" ref="sale_order_18"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">120.50</field>
</record>
<record id="portal_sale_order_1" model="sale.order">
<field name="partner_id" ref="base.partner_demo_portal"/>
<field name="partner_invoice_id" ref="base.partner_demo_portal"/>
<field name="partner_shipping_id" ref="base.partner_demo_portal"/>
<field name="user_id" ref="base.user_admin"/>
<field name="state">sent</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="message_partner_ids" eval="[(4, ref('base.partner_demo_portal'))]"/>
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor4'))]"/>
</record>
<record id="portal_sale_order_line_1" model="sale.order.line">
<field name="order_id" ref="portal_sale_order_1"/>
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_qty">3</field>
<field name="price_unit">295.00</field>
</record>
<record id="portal_sale_order_line_2" model="sale.order.line">
<field name="order_id" ref="portal_sale_order_1"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="product_uom_qty">5</field>
<field name="price_unit">145.00</field>
</record>
<record id="portal_sale_order_line_3" model="sale.order.line">
<field name="order_id" ref="portal_sale_order_1"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="product_uom_qty">2</field>
<field name="price_unit">65.00</field>
</record>
<record id="portal_sale_order_2" model="sale.order">
<field name="partner_id" ref="base.partner_demo_portal"/>
<field name="partner_invoice_id" ref="base.partner_demo_portal"/>
<field name="partner_shipping_id" ref="base.partner_demo_portal"/>
<field name="user_id" ref="base.user_admin"/>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="message_partner_ids" eval="[(4, ref('base.partner_demo_portal'))]"/>
</record>
<record id="portal_sale_order_line_4" model="sale.order.line">
<field name="order_id" ref="portal_sale_order_2"/>
<field name="product_id" ref="product.product_product_1"/>
<field name="product_uom_qty">24</field>
<field name="price_unit">75.00</field>
</record>
<record id="portal_sale_order_line_5" model="sale.order.line">
<field name="order_id" ref="portal_sale_order_2"/>
<field name="product_id" ref="product.product_product_2"/>
<field name="product_uom_qty">30</field>
<field name="price_unit">38.25</field>
</record>
<!-- Confirm some Sales Orders-->
<function model="sale.order" name="action_confirm" eval="[[
ref('sale_order_4'),
ref('sale_order_6'),
ref('sale_order_7'),
ref('sale_order_8'),
ref('sale_order_9'),
ref('sale_order_10'),
ref('sale_order_11'),
ref('sale_order_12'),
ref('sale_order_13'),
ref('sale_order_14'),
ref('sale_order_15'),
ref('sale_order_16'),
ref('sale_order_17'),
ref('sale_order_18'),
ref('portal_sale_order_2'),
]]"/>
<!-- Setting date_order in the past for beautiful spread -->
<record id="sale_order_9" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(days=7)"/>
</record>
<record id="sale_order_11" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(days=21)"/>
</record>
<record id="sale_order_15" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(days=14)"/>
</record>
<record id="sale_order_17" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(days=28)"/>
</record>
<record id="sale_order_18" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(days=35)"/>
</record>
<record id="portal_sale_order_2" model="sale.order">
<field name="date_order" eval="datetime.now() - relativedelta(months=1)"/>
</record>
<!-- Mail messages in SO's chatter -->
<record id="message_sale_1" model="mail.message">
<field name="model">sale.order</field>
<field name="res_id" ref="sale_order_2"/>
<field name="body">Hi,
I have a question regarding services pricing: I heard of a possible discount for quantities exceeding 25 hours.
Could you confirm, please?</field>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_demo"/>
</record>
<record id="message_sale_2" model="mail.message">
<field name="model">sale.order</field>
<field name="res_id" ref="sale_order_2"/>
<field name="parent_id" ref="message_sale_1"/>
<field name="body">Hello,
Unfortunately that was a temporary discount that is not available anymore.
Do you still plan to confirm the order based on the quoted prices?
Thanks!</field>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_root"/>
</record>
<record id="message_sale_3" model="mail.message">
<field name="model">sale.order</field>
<field name="res_id" ref="sale_order_2"/>
<field name="parent_id" ref="message_sale_2"/>
<field name="body">
Alright, thanks for the clarification. I will confirm the order as soon as I get my manager's approval.
</field>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_demo"/>
</record>
<!-- Activities of sales order -->
<record id="sale_activity_2" model="mail.activity">
<field name="res_id" ref="sale.sale_order_3"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="summary">Answer questions</field>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="sale_activity_3" model="mail.activity">
<field name="res_id" ref="sale.sale_order_4"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="sale.mail_act_sale_upsell"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="sale_activity_4" model="mail.activity">
<field name="res_id" ref="sale.sale_order_5"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="sale_activity_6" model="mail.activity">
<field name="res_id" ref="sale.sale_order_7"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="summary">Check delivery requirements</field>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="sale_activity_7" model="mail.activity">
<field name="res_id" ref="sale.sale_order_10"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="summary">Confirm Delivery</field>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="sale_activity_8" model="mail.activity">
<field name="res_id" ref="sale.sale_order_12"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="sale_activity_9" model="mail.activity">
<field name="res_id" ref="sale.sale_order_16"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="sale.mail_act_sale_upsell"/>
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
<field name="create_uid" ref="base.user_demo"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="sale_activity_10" model="mail.activity">
<field name="res_id" ref="sale.portal_sale_order_1"/>
<field name="res_model_id" ref="sale.model_sale_order"/>
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
<field name="summary">Get quote confirmation</field>
<field name="create_uid" ref="base.user_admin"/>
<field name="user_id" ref="base.user_admin"/>
</record>
</odoo>

4588
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

4584
i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

5669
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

4598
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

5239
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

4589
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

5387
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

5700
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

5686
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

5761
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

4605
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

4871
i18n/en_AU.po Normal file

File diff suppressed because it is too large Load Diff

4587
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

5741
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

5741
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

4587
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

4590
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

4590
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

4587
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

4588
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

5663
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

5314
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

5396
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

4587
i18n/fo.po Normal file

File diff suppressed because it is too large Load Diff

5738
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

4594
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

5290
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

4640
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

5298
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

5713
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

4592
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

5729
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

5551
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

4590
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

4586
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

4590
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

5575
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

4588
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

4587
i18n/lo.po Normal file

File diff suppressed because it is too large Load Diff

5341
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

5759
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

4591
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

4629
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

4620
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

5731
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

5407
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

5281
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

5711
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

4638
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

5754
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

5164
i18n/sale.pot Normal file

File diff suppressed because it is too large Load Diff

5318
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

5280
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

4588
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

5489
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

4590
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

5370
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

5658
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

5391
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

5380
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

5702
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

5545
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

5545
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

19
models/__init__.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import analytic
from . import account_move
from . import account_move_line
from . import crm_team
from . import onboarding_onboarding
from . import onboarding_onboarding_step
from . import payment_provider
from . import payment_transaction
from . import product_document
from . import product_product
from . import product_template
from . import res_company
from . import res_partner
from . import sale_order
from . import sale_order_line
from . import utm_campaign

168
models/account_move.py Normal file
View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountMove(models.Model):
_name = 'account.move'
_inherit = ['account.move', 'utm.mixin']
@api.model
def _get_invoice_default_sale_team(self):
return self.env['crm.team']._get_default_team_id()
team_id = fields.Many2one(
'crm.team', string='Sales Team', default=_get_invoice_default_sale_team,
compute='_compute_team_id', store=True, readonly=False,
ondelete="set null", tracking=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
campaign_id = fields.Many2one(ondelete='set null')
medium_id = fields.Many2one(ondelete='set null')
source_id = fields.Many2one(ondelete='set null')
sale_order_count = fields.Integer(compute="_compute_origin_so_count", string='Sale Order Count')
def unlink(self):
downpayment_lines = self.mapped('line_ids.sale_line_ids').filtered(lambda line: line.is_downpayment and line.invoice_lines <= self.mapped('line_ids'))
res = super(AccountMove, self).unlink()
if downpayment_lines:
downpayment_lines.unlink()
return res
@api.depends('invoice_user_id')
def _compute_team_id(self):
for move in self:
if not move.invoice_user_id.sale_team_id or not move.is_sale_document(include_receipts=True):
continue
move.team_id = self.env['crm.team']._get_default_team_id(
user_id=move.invoice_user_id.id,
domain=[('company_id', '=', move.company_id.id)])
@api.depends('line_ids.sale_line_ids')
def _compute_origin_so_count(self):
for move in self:
move.sale_order_count = len(move.line_ids.sale_line_ids.order_id)
def _reverse_moves(self, default_values_list=None, cancel=False):
# OVERRIDE
if not default_values_list:
default_values_list = [{} for move in self]
for move, default_values in zip(self, default_values_list):
default_values.update({
'campaign_id': move.campaign_id.id,
'medium_id': move.medium_id.id,
'source_id': move.source_id.id,
})
return super()._reverse_moves(default_values_list=default_values_list, cancel=cancel)
def action_post(self):
# inherit of the function from account.move to validate a new tax and the priceunit of a downpayment
res = super(AccountMove, self).action_post()
# We cannot change lines content on locked SO, changes on invoices are not forwarded to the SO if the SO is locked
downpayment_lines = self.line_ids.sale_line_ids.filtered(lambda l: l.is_downpayment and not l.display_type and not l.order_id.locked)
other_so_lines = downpayment_lines.order_id.order_line - downpayment_lines
real_invoices = set(other_so_lines.invoice_lines.move_id)
for so_dpl in downpayment_lines:
so_dpl.price_unit = sum(
l.price_unit if l.move_id.move_type == 'out_invoice' else -l.price_unit
for l in so_dpl.invoice_lines
if l.move_id.state == 'posted' and l.move_id not in real_invoices # don't recompute with the final invoice
)
so_dpl.tax_id = so_dpl.invoice_lines.tax_ids
return res
def button_draft(self):
res = super().button_draft()
self.line_ids.filtered('is_downpayment').sale_line_ids.filtered(
lambda sol: not sol.display_type)._compute_name()
return res
def button_cancel(self):
res = super().button_cancel()
self.line_ids.filtered('is_downpayment').sale_line_ids.filtered(
lambda sol: not sol.display_type)._compute_name()
return res
def _post(self, soft=True):
# OVERRIDE
# Auto-reconcile the invoice with payments coming from transactions.
# It's useful when you have a "paid" sale order (using a payment transaction) and you invoice it later.
posted = super()._post(soft)
for invoice in posted.filtered(lambda move: move.is_invoice()):
payments = invoice.mapped('transaction_ids.payment_id').filtered(lambda x: x.state == 'posted')
move_lines = payments.line_ids.filtered(lambda line: line.account_type in ('asset_receivable', 'liability_payable') and not line.reconciled)
for line in move_lines:
invoice.js_assign_outstanding_line(line.id)
return posted
def _invoice_paid_hook(self):
# OVERRIDE
res = super(AccountMove, self)._invoice_paid_hook()
todo = set()
for invoice in self.filtered(lambda move: move.is_invoice()):
for line in invoice.invoice_line_ids:
for sale_line in line.sale_line_ids:
todo.add((sale_line.order_id, invoice.name))
for (order, name) in todo:
order.message_post(body=_("Invoice %s paid", name))
return res
def _action_invoice_ready_to_be_sent(self):
# OVERRIDE
# Make sure the send invoice CRON is called when an invoice becomes ready to be sent by mail.
res = super()._action_invoice_ready_to_be_sent()
send_invoice_cron = self.env.ref('sale.send_invoice_cron', raise_if_not_found=False)
if send_invoice_cron:
send_invoice_cron._trigger()
return res
def action_view_source_sale_orders(self):
self.ensure_one()
source_orders = self.line_ids.sale_line_ids.order_id
result = self.env['ir.actions.act_window']._for_xml_id('sale.action_orders')
if len(source_orders) > 1:
result['domain'] = [('id', 'in', source_orders.ids)]
elif len(source_orders) == 1:
result['views'] = [(self.env.ref('sale.view_order_form', False).id, 'form')]
result['res_id'] = source_orders.id
else:
result = {'type': 'ir.actions.act_window_close'}
return result
def _is_downpayment(self):
# OVERRIDE
self.ensure_one()
return self.line_ids.sale_line_ids and all(sale_line.is_downpayment for sale_line in self.line_ids.sale_line_ids) or False
@api.depends('line_ids.sale_line_ids.order_id', 'currency_id', 'tax_totals', 'date')
def _compute_partner_credit(self):
super()._compute_partner_credit()
for move in self.filtered(lambda m: m.is_invoice(include_receipts=True)):
sale_orders = move.line_ids.sale_line_ids.order_id
amount_total_currency = move.currency_id._convert(
move.tax_totals['amount_total'],
move.company_currency_id,
move.company_id,
move.date
)
amount_to_invoice_currency = sum(
sale_order.currency_id._convert(
sale_order.amount_to_invoice,
move.company_currency_id,
move.company_id,
move.date
) for sale_order in sale_orders
)
move.partner_credit += max(amount_total_currency - amount_to_invoice_currency, 0.0)

229
models/account_move_line.py Normal file
View File

@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
is_downpayment = fields.Boolean()
sale_line_ids = fields.Many2many(
'sale.order.line',
'sale_order_line_invoice_rel',
'invoice_line_id', 'order_line_id',
string='Sales Order Lines', readonly=True, copy=False)
def _copy_data_extend_business_fields(self, values):
# OVERRIDE to copy the 'sale_line_ids' field as well.
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
values['sale_line_ids'] = [(6, None, self.sale_line_ids.ids)]
def _prepare_analytic_lines(self):
""" Note: This method is called only on the move.line that having an analytic distribution, and
so that should create analytic entries.
"""
values_list = super(AccountMoveLine, self)._prepare_analytic_lines()
# filter the move lines that can be reinvoiced: a cost (negative amount) analytic line without SO line but with a product can be reinvoiced
move_to_reinvoice = self.env['account.move.line']
if len(values_list) > 0:
for index, move_line in enumerate(self):
values = values_list[index]
if 'so_line' not in values:
if move_line._sale_can_be_reinvoice():
move_to_reinvoice |= move_line
# insert the sale line in the create values of the analytic entries
if move_to_reinvoice.filtered(lambda aml: not aml.move_id.reversed_entry_id): # only if the move line is not a reversal one
map_sale_line_per_move = move_to_reinvoice._sale_create_reinvoice_sale_line()
for values in values_list:
sale_line = map_sale_line_per_move.get(values.get('move_line_id'))
if sale_line:
values['so_line'] = sale_line.id
return values_list
def _sale_can_be_reinvoice(self):
""" determine if the generated analytic line should be reinvoiced or not.
For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
"""
self.ensure_one()
if self.sale_line_ids:
return False
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits) != 1 and self.product_id.expense_policy not in [False, 'no']
def _sale_create_reinvoice_sale_line(self):
sale_order_map = self._sale_determine_order()
sale_line_values_to_create = [] # the list of creation values of sale line to create.
existing_sale_line_cache = {} # in the sales_price-delivery case, we can reuse the same sale line. This cache will avoid doing a search each time the case happen
# `map_move_sale_line` is map where
# - key is the move line identifier
# - value is either a sale.order.line record (existing case), or an integer representing the index of the sale line to create in
# the `sale_line_values_to_create` (not existing case, which will happen more often than the first one).
map_move_sale_line = {}
for move_line in self:
sale_order = sale_order_map.get(move_line.id)
# no reinvoice as no sales order was found
if not sale_order:
continue
# raise if the sale order is not currently open
if sale_order.state in ('draft', 'sent'):
raise UserError(_(
"The Sales Order %(order)s linked to the Analytic Account %(account)s must be"
" validated before registering expenses.",
order=sale_order.name,
account=sale_order.analytic_account_id.name,
))
elif sale_order.state == 'cancel':
raise UserError(_(
"The Sales Order %(order)s linked to the Analytic Account %(account)s is cancelled."
" You cannot register an expense on a cancelled Sales Order.",
order=sale_order.name,
account=sale_order.analytic_account_id.name,
))
elif sale_order.locked:
raise UserError(_(
"The Sales Order %(order)s linked to the Analytic Account %(account)s is currently locked."
" You cannot register an expense on a locked Sales Order."
" Please create a new SO linked to this Analytic Account.",
order=sale_order.name,
account=sale_order.analytic_account_id.name,
))
price = move_line._sale_get_invoice_price(sale_order)
# find the existing sale.line or keep its creation values to process this in batch
sale_line = None
if (
move_line.product_id.expense_policy == 'sales_price'
and move_line.product_id.invoice_policy == 'delivery'
and not self.env.context.get('force_split_lines')
):
# for those case only, we can try to reuse one
map_entry_key = (sale_order.id, move_line.product_id.id, price) # cache entry to limit the call to search
sale_line = existing_sale_line_cache.get(map_entry_key)
if sale_line: # already search, so reuse it. sale_line can be sale.order.line record or index of a "to create values" in `sale_line_values_to_create`
map_move_sale_line[move_line.id] = sale_line
existing_sale_line_cache[map_entry_key] = sale_line
else: # search for existing sale line
sale_line = self.env['sale.order.line'].search([
('order_id', '=', sale_order.id),
('price_unit', '=', price),
('product_id', '=', move_line.product_id.id),
('is_expense', '=', True),
], limit=1)
if sale_line: # found existing one, so keep the browse record
map_move_sale_line[move_line.id] = existing_sale_line_cache[map_entry_key] = sale_line
else: # should be create, so use the index of creation values instead of browse record
# save value to create it
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
# store it in the cache of existing ones
existing_sale_line_cache[map_entry_key] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
# store it in the map_move_sale_line map
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
else: # save its value to create it anyway
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
# create the sale lines in batch
new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create)
# build result map by replacing index with newly created record of sale.order.line
result = {}
for move_line_id, unknown_sale_line in map_move_sale_line.items():
if isinstance(unknown_sale_line, int): # index of newly created sale line
result[move_line_id] = new_sale_lines[unknown_sale_line]
elif isinstance(unknown_sale_line, models.BaseModel): # already record of sale.order.line
result[move_line_id] = unknown_sale_line
return result
def _sale_determine_order(self):
""" Get the mapping of move.line with the sale.order record on which its analytic entries should be reinvoiced
:return a dict where key is the move line id, and value is sale.order record (or None).
"""
mapping = {}
for move_line in self:
if move_line.analytic_distribution:
distribution_json = move_line.analytic_distribution
account_ids = [int(account_id) for key in distribution_json.keys() for account_id in key.split(',')]
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', account_ids),
('state', '=', 'sale')], order='create_date ASC', limit=1)
if sale_order:
mapping[move_line.id] = sale_order
else:
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', account_ids)],
order='create_date ASC', limit=1)
mapping[move_line.id] = sale_order
# map of AAL index with the SO on which it needs to be reinvoiced. Maybe be None if no SO found
return mapping
def _sale_prepare_sale_line_values(self, order, price):
""" Generate the sale.line creation value from the current move line """
self.ensure_one()
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
last_sequence = last_so_line.sequence + 1 if last_so_line else 100
fpos = order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)
product_taxes = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == order.company_id)
taxes = fpos.map_tax(product_taxes)
return {
'order_id': order.id,
'name': self.name,
'sequence': last_sequence,
'price_unit': price,
'tax_id': [x.id for x in taxes],
'discount': 0.0,
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': 0.0,
'is_expense': True,
}
def _sale_get_invoice_price(self, order):
""" Based on the current move line, compute the price to reinvoice the analytic line that is going to be created (so the
price of the sale line).
"""
self.ensure_one()
unit_amount = self.quantity
amount = (self.credit or 0.0) - (self.debit or 0.0)
if self.product_id.expense_policy == 'sales_price':
return order.pricelist_id._get_product_price(
self.product_id,
1.0,
uom=self.product_uom_id,
date=order.date_order,
)
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if float_is_zero(unit_amount, precision_digits=uom_precision_digits):
return 0.0
# Prevent unnecessary currency conversion that could be impacted by exchange rate
# fluctuations
if self.company_id.currency_id and amount and self.company_id.currency_id == order.currency_id:
return self.company_id.currency_id.round(abs(amount / unit_amount))
price_unit = abs(amount / unit_amount)
currency_id = self.company_id.currency_id
if currency_id and currency_id != order.currency_id:
price_unit = currency_id._convert(price_unit, order.currency_id, order.company_id, order.date_order or fields.Date.today())
return price_unit
def _get_downpayment_lines(self):
# OVERRIDE
return self.sale_line_ids.filtered('is_downpayment').invoice_lines.filtered(lambda line: line.move_id._is_downpayment())

34
models/analytic.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
# [XBO] TODO: remove me in master
allowed_so_line_ids = fields.Many2many('sale.order.line', compute='_compute_allowed_so_line_ids')
so_line = fields.Many2one('sale.order.line', string='Sales Order Item', domain=[('qty_delivered_method', '=', 'analytic')])
def _default_sale_line_domain(self):
""" This is only used for delivered quantity of SO line based on analytic line, and timesheet
(see sale_timesheet). This can be override to allow further customization.
[XBO] TODO: remove me in master
"""
return [('qty_delivered_method', '=', 'analytic')]
def _compute_allowed_so_line_ids(self):
# [XBO] TODO: remove me in master
self.allowed_so_line_ids = False
class AccountAnalyticApplicability(models.Model):
_inherit = 'account.analytic.applicability'
_description = "Analytic Plan's Applicabilities"
business_domain = fields.Selection(
selection_add=[
('sale_order', 'Sale Order'),
],
ondelete={'sale_order': 'cascade'},
)

166
models/crm_team.py Normal file
View File

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class CrmTeam(models.Model):
_inherit = 'crm.team'
invoiced = fields.Float(
compute='_compute_invoiced',
string='Invoiced This Month', readonly=True,
help="Invoice revenue for the current month. This is the amount the sales "
"channel has invoiced this month. It is used to compute the progression ratio "
"of the current and target revenue on the kanban view.")
invoiced_target = fields.Float(
string='Invoicing Target',
help="Revenue target for the current month (untaxed total of confirmed invoices).")
quotations_count = fields.Integer(
compute='_compute_quotations_to_invoice',
string='Number of quotations to invoice', readonly=True)
quotations_amount = fields.Float(
compute='_compute_quotations_to_invoice',
string='Amount of quotations to invoice', readonly=True)
sales_to_invoice_count = fields.Integer(
compute='_compute_sales_to_invoice',
string='Number of sales to invoice', readonly=True)
sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='# Sale Orders')
def _compute_quotations_to_invoice(self):
query = self.env['sale.order']._where_calc([
('team_id', 'in', self.ids),
('state', 'in', ['draft', 'sent']),
])
self.env['sale.order']._apply_ir_rules(query, 'read')
_, where_clause, where_clause_args = query.get_sql()
select_query = """
SELECT team_id, count(*), sum(amount_total /
CASE COALESCE(currency_rate, 0)
WHEN 0 THEN 1.0
ELSE currency_rate
END
) as amount_total
FROM sale_order
WHERE %s
GROUP BY team_id
""" % where_clause
self.env.cr.execute(select_query, where_clause_args)
quotation_data = self.env.cr.dictfetchall()
teams = self.browse()
for datum in quotation_data:
team = self.browse(datum['team_id'])
team.quotations_amount = datum['amount_total']
team.quotations_count = datum['count']
teams |= team
remaining = (self - teams)
remaining.quotations_amount = 0
remaining.quotations_count = 0
def _compute_sales_to_invoice(self):
sale_order_data = self.env['sale.order']._read_group([
('team_id', 'in', self.ids),
('invoice_status','=','to invoice'),
], ['team_id'], ['__count'])
data_map = {team.id: count for team, count in sale_order_data}
for team in self:
team.sales_to_invoice_count = data_map.get(team.id,0.0)
def _compute_invoiced(self):
if not self:
return
query = '''
SELECT
move.team_id AS team_id,
SUM(move.amount_untaxed_signed) AS amount_untaxed_signed
FROM account_move move
WHERE move.move_type IN ('out_invoice', 'out_refund', 'out_receipt')
AND move.payment_state IN ('in_payment', 'paid', 'reversed')
AND move.state = 'posted'
AND move.team_id IN %s
AND move.date BETWEEN %s AND %s
GROUP BY move.team_id
'''
today = fields.Date.today()
params = [tuple(self.ids), fields.Date.to_string(today.replace(day=1)), fields.Date.to_string(today)]
self._cr.execute(query, params)
data_map = dict((v[0], v[1]) for v in self._cr.fetchall())
for team in self:
team.invoiced = data_map.get(team.id, 0.0)
def _compute_sale_order_count(self):
sale_order_data = self.env['sale.order']._read_group([
('team_id', 'in', self.ids),
('state', '!=', 'cancel'),
], ['team_id'], ['__count'])
data_map = {team.id: count for team, count in sale_order_data}
for team in self:
team.sale_order_count = data_map.get(team.id, 0)
def _in_sale_scope(self):
return self.env.context.get('in_sales_app')
def _graph_get_model(self):
if self._in_sale_scope():
return 'sale.report'
return super()._graph_get_model()
def _graph_date_column(self):
if self._in_sale_scope():
return 'date'
return super()._graph_date_column()
def _graph_get_table(self, GraphModel):
if self._in_sale_scope():
# For a team not shared between company, we make sure the amounts are expressed
# in the currency of the team company and not converted to the current company currency,
# as the amounts of the sale report are converted in the currency
# of the current company (for multi-company reporting, see #83550)
GraphModel = GraphModel.with_company(self.company_id)
return f"({GraphModel._table_query}) AS {GraphModel._table}"
return super()._graph_get_table(GraphModel)
def _graph_y_query(self):
if self._in_sale_scope():
return 'SUM(price_subtotal)'
return super()._graph_y_query()
def _extra_sql_conditions(self):
if self._in_sale_scope():
return "AND state = 'sale'"
return super()._extra_sql_conditions()
def _graph_title_and_key(self):
if self._in_sale_scope():
return ['', _('Sales: Untaxed Total')] # no more title
return super()._graph_title_and_key()
def _compute_dashboard_button_name(self):
super(CrmTeam,self)._compute_dashboard_button_name()
if self._in_sale_scope():
self.dashboard_button_name = _("Sales Analysis")
def action_primary_channel_button(self):
if self._in_sale_scope():
return self.env["ir.actions.actions"]._for_xml_id("sale.action_order_report_so_salesteam")
return super().action_primary_channel_button()
def update_invoiced_target(self, value):
return self.write({'invoiced_target': round(float(value or 0))})
@api.ondelete(at_uninstall=False)
def _unlink_except_used_for_sales(self):
""" If more than 5 active SOs, we consider this team to be actively used.
5 is some random guess based on "user testing", aka more than testing
CRM feature and less than use it in real life use cases. """
SO_COUNT_TRIGGER = 5
for team in self:
if team.sale_order_count >= SO_COUNT_TRIGGER:
raise UserError(
_('Team %(team_name)s has %(sale_order_count)s active sale orders. Consider canceling them or archiving the team instead.',
team_name=team.name,
sale_order_count=team.sale_order_count
))

View File

@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class Onboarding(models.Model):
_inherit = 'onboarding.onboarding'
# Sale Quotation Onboarding
@api.model
def action_close_panel_sale_quotation(self):
self.action_close_panel('sale.onboarding_onboarding_sale_quotation')

View File

@ -0,0 +1,98 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import _, api, Command, models
from odoo.tools import file_open
class OnboardingStep(models.Model):
_inherit = 'onboarding.onboarding.step'
@api.model
def action_open_step_sale_order_confirmation(self):
self.env.company.get_chart_of_accounts_or_fail()
action = {
'type': 'ir.actions.act_window',
'name': _('Choose how to confirm quotations'),
'res_model': 'sale.payment.provider.onboarding.wizard',
'view_mode': 'form',
'views': [(self.env.ref('payment.payment_provider_onboarding_wizard_form').id, 'form')],
'target': 'new',
}
return action
@api.model
def _get_sample_sales_order(self):
""" Get a sample quotation or create one if it does not exist. """
# use current user as partner
partner = self.env.user.partner_id
company_id = self.env.company.id
# is there already one?
sample_sales_order = self.env['sale.order'].search([
('company_id', '=', company_id),
('partner_id', '=', partner.id),
('state', '=', 'draft'),
], limit=1)
if not sample_sales_order:
# take any existing product or create one
product = self.env['product.product'].search([], limit=1)
if not product:
with file_open('product/static/img/product_product_13-image.jpg', 'rb') as default_image_stream:
product = self.env['product.product'].create({
'name': _('Sample Product'),
'active': False,
'image_1920': base64.b64encode(default_image_stream.read()),
})
product.product_tmpl_id.active = False
sample_sales_order = self.env['sale.order'].create({
'partner_id': partner.id,
'order_line': [
Command.create({
'name': _('Sample Order Line'),
'product_id': product.id,
'product_uom_qty': 10,
'price_unit': 123,
})
]
})
return sample_sales_order
@api.model
def action_open_step_sample_quotation(self):
""" Onboarding step for sending a sample quotation. Open a window to compose an email,
with the edi_invoice_template message loaded by default. """
sample_sales_order = self._get_sample_sales_order()
template = self.env.ref('sale.email_template_edi_sale', False)
self.env['mail.compose.message'].with_context(
mark_so_as_sent=True,
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
proforma=self.env.context.get('proforma', False),
).create({
'res_ids': sample_sales_order.ids,
'template_id': template.id if template else False,
'model': sample_sales_order._name,
'composition_mode': 'comment',
})._action_send_mail()
self.action_validate_step('sale.onboarding_onboarding_step_sample_quotation')
sale_quotation_onboarding = self.env.ref('sale.onboarding_onboarding_sale_quotation', raise_if_not_found=False)
if sale_quotation_onboarding:
sale_quotation_onboarding.action_close()
view_id = self.env.ref('sale.view_order_form').id
action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders')
action.update({
'view_mode': 'form',
'views': [[view_id, 'form']],
'target': 'main',
})
return action
@api.model
def action_validate_step_payment_provider(self):
validation_response = super().action_validate_step_payment_provider()
if self.env.company.sale_onboarding_payment_method: # Set if the flow is/was done from the Sales panel
return self.action_validate_step('sale.onboarding_onboarding_step_sale_order_confirmation')
return validation_response

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class PaymentProvider(models.Model):
_inherit = 'payment.provider'
so_reference_type = fields.Selection(string='Communication',
selection=[
('so_name', 'Based on Document Reference'),
('partner', 'Based on Customer ID')], default='so_name',
help='You can set here the communication type that will appear on sales orders.'
'The communication will be given to the customer when they choose the payment method.')

View File

@ -0,0 +1,235 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil import relativedelta
from odoo import _, api, Command, fields, models, SUPERUSER_ID
from odoo.tools import str2bool
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
sale_order_ids = fields.Many2many('sale.order', 'sale_order_transaction_rel', 'transaction_id', 'sale_order_id',
string='Sales Orders', copy=False, readonly=True)
sale_order_ids_nbr = fields.Integer(compute='_compute_sale_order_ids_nbr', string='# of Sales Orders')
def _compute_sale_order_reference(self, order):
self.ensure_one()
if self.provider_id.so_reference_type == 'so_name':
order_reference = order.name
else:
# self.provider_id.so_reference_type == 'partner'
identification_number = order.partner_id.id
order_reference = '%s/%s' % ('CUST', str(identification_number % 97).rjust(2, '0'))
invoice_journal = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
if invoice_journal:
order_reference = invoice_journal._process_reference_for_sale_order(order_reference)
return order_reference
@api.depends('sale_order_ids')
def _compute_sale_order_ids_nbr(self):
for trans in self:
trans.sale_order_ids_nbr = len(trans.sale_order_ids)
def _set_pending(self, state_message=None, **kwargs):
""" Override of `payment` to send the quotations automatically.
:param str state_message: The reason for which the transaction is set in 'pending' state.
:return: updated transactions.
:rtype: `payment.transaction` recordset.
"""
txs_to_process = super()._set_pending(state_message=state_message, **kwargs)
for tx in txs_to_process: # Consider only transactions that are indeed set pending.
sales_orders = tx.sale_order_ids.filtered(lambda so: so.state in ['draft', 'sent'])
sales_orders.filtered(
lambda so: so.state == 'draft'
).with_context(tracking_disable=True).action_quotation_sent()
if tx.provider_id.code == 'custom':
for so in tx.sale_order_ids:
so.reference = tx._compute_sale_order_reference(so)
# Send the payment status email.
# The transactions are manually cached while in a sudoed environment to prevent an
# AccessError: In some circumstances, sending the mail would generate the report assets
# during the rendering of the mail body, causing a cursor commit, a flush, and forcing
# the re-computation of the pending computed fields of the `mail.compose.message`,
# including part of the template. Since that template reads the order's transactions and
# the re-computation of the field is not done with the same environment, reading fields
# that were not already available in the cache could trigger an AccessError (e.g., if
# the payment was initiated by a public user).
sales_orders.mapped('transaction_ids')
sales_orders._send_payment_succeeded_for_order_mail()
return txs_to_process
def _check_amount_and_confirm_order(self):
""" Confirm the sales order based on the amount of a transaction.
Confirm the sales orders only if the transaction amount (or the sum of the partial
transaction amounts) is equal to or greater than the required amount for order confirmation
Grouped payments (paying multiple sales orders in one transaction) are not supported.
:return: The confirmed sales orders.
:rtype: a `sale.order` recordset
"""
confirmed_orders = self.env['sale.order']
for tx in self:
# We only support the flow where exactly one quotation is linked to a transaction.
if len(tx.sale_order_ids) == 1:
quotation = tx.sale_order_ids.filtered(lambda so: so.state in ('draft', 'sent'))
if quotation and quotation._is_confirmation_amount_reached():
quotation.with_context(send_email=True).action_confirm()
confirmed_orders |= quotation
return confirmed_orders
def _set_authorized(self, state_message=None, **kwargs):
""" Override of payment to confirm the quotations automatically. """
super()._set_authorized(state_message=state_message, **kwargs)
confirmed_orders = self._check_amount_and_confirm_order()
confirmed_orders._send_order_confirmation_mail()
(self.sale_order_ids - confirmed_orders)._send_payment_succeeded_for_order_mail()
def _log_message_on_linked_documents(self, message):
""" Override of payment to log a message on the sales orders linked to the transaction.
Note: self.ensure_one()
:param str message: The message to be logged
:return: None
"""
super()._log_message_on_linked_documents(message)
self = self.with_user(SUPERUSER_ID) # Log messages as 'OdooBot'
for order in self.sale_order_ids or self.source_transaction_id.sale_order_ids:
order.message_post(body=message)
def _reconcile_after_done(self):
""" Override of payment to automatically confirm quotations and generate invoices. """
confirmed_orders = self._check_amount_and_confirm_order()
confirmed_orders._send_order_confirmation_mail()
(self.sale_order_ids - confirmed_orders)._send_payment_succeeded_for_order_mail()
auto_invoice = str2bool(
self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'))
if auto_invoice:
# Invoice the sale orders in self instead of in confirmed_orders to create the invoice
# even if only a partial payment was made.
self._invoice_sale_orders()
super()._reconcile_after_done()
if auto_invoice:
# Must be called after the super() call to make sure the invoice are correctly posted.
self._send_invoice()
def _send_invoice(self):
template_id = int(self.env['ir.config_parameter'].sudo().get_param(
'sale.default_invoice_email_template',
default=0
))
if not template_id:
return
template = self.env['mail.template'].browse(template_id).exists()
if not template:
return
for tx in self:
tx = tx.with_company(tx.company_id).with_context(
company_id=tx.company_id.id,
)
invoice_to_send = tx.invoice_ids.filtered(
lambda i: not i.is_move_sent and i.state == 'posted' and i._is_ready_to_be_sent()
)
invoice_to_send.is_move_sent = True # Mark invoice as sent
invoice_to_send.with_user(SUPERUSER_ID)._generate_pdf_and_send_invoice(template)
def _cron_send_invoice(self):
"""
Cron to send invoice that where not ready to be send directly after posting
"""
if not self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
return
# No need to retrieve old transactions
retry_limit_date = datetime.now() - relativedelta.relativedelta(days=2)
# Retrieve all transactions matching the criteria for post-processing
self.search([
('state', '=', 'done'),
('is_post_processed', '=', True),
('invoice_ids', 'in', self.env['account.move']._search([
('is_move_sent', '=', False),
('state', '=', 'posted'),
])),
('sale_order_ids.state', '=', 'sale'),
('last_state_change', '>=', retry_limit_date),
])._send_invoice()
def _invoice_sale_orders(self):
for tx in self.filtered(lambda tx: tx.sale_order_ids):
tx = tx.with_company(tx.company_id)
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state == 'sale')
if confirmed_orders:
# Filter orders between those fully paid and those partially paid.
fully_paid_orders = confirmed_orders.filtered(lambda so: so._is_paid())
# Create a down payment invoice for partially paid orders
downpayment_invoices = (
confirmed_orders - fully_paid_orders
)._generate_downpayment_invoices()
# For fully paid orders create a final invoice.
fully_paid_orders._force_lines_to_invoice_policy_order()
final_invoices = fully_paid_orders.with_context(
raise_if_nothing_to_invoice=False
)._create_invoices(final=True)
invoices = downpayment_invoices + final_invoices
# Setup access token in advance to avoid serialization failure between
# edi postprocessing of invoice and displaying the sale order on the portal
for invoice in invoices:
invoice._portal_ensure_token()
tx.invoice_ids = [Command.set(invoices.ids)]
@api.model
def _compute_reference_prefix(self, provider_code, separator, **values):
""" Override of payment to compute the reference prefix based on Sales-specific values.
If the `values` parameter has an entry with 'sale_order_ids' as key and a list of (4, id, O)
or (6, 0, ids) X2M command as value, the prefix is computed based on the sales order name(s)
Otherwise, the computation is delegated to the super method.
:param str provider_code: The code of the provider handling the transaction
:param str separator: The custom separator used to separate data references
:param dict values: The transaction values used to compute the reference prefix. It should
have the structure {'sale_order_ids': [(X2M command), ...], ...}.
:return: The computed reference prefix if order ids are found, the one of `super` otherwise
:rtype: str
"""
command_list = values.get('sale_order_ids')
if command_list:
# Extract sales order id(s) from the X2M commands
order_ids = self._fields['sale_order_ids'].convert_to_cache(command_list, self)
orders = self.env['sale.order'].browse(order_ids).exists()
if len(orders) == len(order_ids): # All ids are valid
return separator.join(orders.mapped('name'))
return super()._compute_reference_prefix(provider_code, separator, **values)
def action_view_sales_orders(self):
action = {
'name': _('Sales Order(s)'),
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'target': 'current',
}
sale_order_ids = self.sale_order_ids.ids
if len(sale_order_ids) == 1:
action['res_id'] = sale_order_ids[0]
action['view_mode'] = 'form'
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', sale_order_ids)]
return action

View File

@ -0,0 +1,22 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ProductDocument(models.Model):
_inherit = 'product.document'
attached_on = fields.Selection(
selection=[
('quotation', "Quotation"),
('sale_order', "Confirmed order"),
],
string="Visible at",
help="Allows you to share the document with your customers within a sale.\n"
"Leave it empty if you don't want to share this document with sales customer.\n"
"Quotation: the document will be sent to and accessible by customers at any time.\n"
"e.g. this option can be useful to share Product description files.\n"
"Confirmed order: the document will be sent to and accessible by customers.\n"
"e.g. this option can be useful to share User Manual or digital content bought"
" on ecommerce. ",
)

111
models/product_product.py Normal file
View File

@ -0,0 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import time, timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_round
class ProductProduct(models.Model):
_inherit = 'product.product'
sales_count = fields.Float(compute='_compute_sales_count', string='Sold', digits='Product Unit of Measure')
# Catalog related fields
product_catalog_product_is_in_sale_order = fields.Boolean(
compute='_compute_product_is_in_sale_order',
search='_search_product_is_in_sale_order',
)
def _compute_sales_count(self):
r = {}
self.sales_count = 0
if not self.user_has_groups('sales_team.group_sale_salesman'):
return r
date_from = fields.Datetime.to_string(fields.datetime.combine(fields.datetime.now() - timedelta(days=365),
time.min))
done_states = self.env['sale.report']._get_done_states()
domain = [
('state', 'in', done_states),
('product_id', 'in', self.ids),
('date', '>=', date_from),
]
for product, product_uom_qty in self.env['sale.report']._read_group(domain, ['product_id'], ['product_uom_qty:sum']):
r[product.id] = product_uom_qty
for product in self:
if not product.id:
product.sales_count = 0.0
continue
product.sales_count = float_round(r.get(product.id, 0), precision_rounding=product.uom_id.rounding)
return r
@api.onchange('type')
def _onchange_type(self):
if self._origin and self.sales_count > 0:
return {'warning': {
'title': _("Warning"),
'message': _("You cannot change the product's type because it is already used in sales orders.")
}}
@api.depends_context('order_id')
def _compute_product_is_in_sale_order(self):
order_id = self.env.context.get('order_id')
if not order_id:
self.product_catalog_product_is_in_sale_order = False
return
read_group_data = self.env['sale.order.line']._read_group(
domain=[('order_id', '=', order_id)],
groupby=['product_id'],
aggregates=['__count'],
)
data = {product.id: count for product, count in read_group_data}
for product in self:
product.product_catalog_product_is_in_sale_order = bool(data.get(product.id, 0))
def _search_product_is_in_sale_order(self, operator, value):
if operator not in ['=', '!='] or not isinstance(value, bool):
raise UserError(_("Operation not supported"))
product_ids = self.env['sale.order.line'].search([
('order_id', 'in', [self.env.context.get('order_id', '')]),
]).product_id.ids
return [('id', 'in', product_ids)]
def action_view_sales(self):
action = self.env["ir.actions.actions"]._for_xml_id("sale.report_all_channels_sales_action")
action['domain'] = [('product_id', 'in', self.ids)]
action['context'] = {
'pivot_measures': ['product_uom_qty'],
'active_id': self._context.get('active_id'),
'search_default_Sales': 1,
'active_model': 'sale.report',
'search_default_filter_order_date': 1,
}
return action
def _get_invoice_policy(self):
return self.invoice_policy
def _filter_to_unlink(self):
domain = [('product_id', 'in', self.ids)]
lines = self.env['sale.order.line']._read_group(domain, ['product_id'])
linked_product_ids = [product.id for [product] in lines]
return super(ProductProduct, self - self.browse(linked_product_ids))._filter_to_unlink()
class ProductAttributeCustomValue(models.Model):
_inherit = "product.attribute.custom.value"
sale_order_line_id = fields.Many2one('sale.order.line', string="Sales Order Line", ondelete='cascade')
_sql_constraints = [
('sol_custom_value_unique', 'unique(custom_product_template_attribute_value_id, sale_order_line_id)', "Only one Custom Value is allowed per Attribute Value per Sales Order Line.")
]
class ProductPackaging(models.Model):
_inherit = 'product.packaging'
sales = fields.Boolean("Sales", default=True, help="If true, the packaging can be used for sales orders")

144
models/product_template.py Normal file
View File

@ -0,0 +1,144 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_round
class ProductTemplate(models.Model):
_inherit = 'product.template'
service_type = fields.Selection(
selection=[('manual', "Manually set quantities on order")],
string="Track Service",
compute='_compute_service_type', store=True, readonly=False, precompute=True,
help="Manually set quantities on order: Invoice based on the manually entered quantity, without creating an analytic account.\n"
"Timesheets on contract: Invoice based on the tracked hours on the related timesheet.\n"
"Create a task and track hours: Create a task on the sales order validation and track the work hours.")
sale_line_warn = fields.Selection(
WARNING_MESSAGE, string="Sales Order Line",
help=WARNING_HELP, required=True, default="no-message")
sale_line_warn_msg = fields.Text(string="Message for Sales Order Line")
expense_policy = fields.Selection(
selection=[
('no', "No"),
('cost', "At cost"),
('sales_price', "Sales price"),
],
string="Re-Invoice Expenses", default='no',
compute='_compute_expense_policy', store=True, readonly=False,
help="Validated expenses and vendor bills can be re-invoiced to a customer at its cost or sales price.")
visible_expense_policy = fields.Boolean(
string="Re-Invoice Policy visible", compute='_compute_visible_expense_policy')
sales_count = fields.Float(
string="Sold", compute='_compute_sales_count', digits='Product Unit of Measure')
invoice_policy = fields.Selection(
selection=[
('order', "Ordered quantities"),
('delivery', "Delivered quantities"),
],
string="Invoicing Policy",
compute='_compute_invoice_policy', store=True, readonly=False, precompute=True,
help="Ordered Quantity: Invoice quantities ordered by the customer.\n"
"Delivered Quantity: Invoice quantities delivered to the customer.")
@api.depends('name')
def _compute_visible_expense_policy(self):
visibility = self.user_has_groups('analytic.group_analytic_accounting')
for product_template in self:
product_template.visible_expense_policy = visibility
@api.depends('sale_ok')
def _compute_expense_policy(self):
self.filtered(lambda t: not t.sale_ok).expense_policy = 'no'
@api.depends('product_variant_ids.sales_count')
def _compute_sales_count(self):
for product in self:
product.sales_count = float_round(sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]), precision_rounding=product.uom_id.rounding)
@api.constrains('company_id')
def _check_sale_product_company(self):
"""Ensure the product is not being restricted to a single company while
having been sold in another one in the past, as this could cause issues."""
target_company = self.company_id
if target_company: # don't prevent writing `False`, should always work
subquery_products = self.env['product.product'].sudo().with_context(active_test=False)._search([('product_tmpl_id', 'in', self.ids)])
so_lines = self.env['sale.order.line'].sudo().search_read(
[('product_id', 'in', subquery_products), '!', ('company_id', 'child_of', target_company.root_id.id)],
fields=['id', 'product_id'],
)
used_products = list(map(lambda sol: sol['product_id'][1], so_lines))
if so_lines:
raise ValidationError(_('The following products cannot be restricted to the company'
' %s because they have already been used in quotations or '
'sales orders in another company:\n%s\n'
'You can archive these products and recreate them '
'with your company restriction instead, or leave them as '
'shared product.', target_company.name, ', '.join(used_products)))
def action_view_sales(self):
action = self.env['ir.actions.actions']._for_xml_id('sale.report_all_channels_sales_action')
action['domain'] = [('product_tmpl_id', 'in', self.ids)]
action['context'] = {
'pivot_measures': ['product_uom_qty'],
'active_id': self._context.get('active_id'),
'active_model': 'sale.report',
'search_default_Sales': 1,
'search_default_filter_order_date': 1,
}
return action
@api.onchange('type')
def _onchange_type(self):
res = super(ProductTemplate, self)._onchange_type()
if self._origin and self.sales_count > 0:
res['warning'] = {
'title': _("Warning"),
'message': _("You cannot change the product's type because it is already used in sales orders.")
}
return res
@api.depends('type')
def _compute_service_type(self):
self.filtered(lambda t: t.type == 'consu' or not t.service_type).service_type = 'manual'
@api.depends('type')
def _compute_invoice_policy(self):
self.filtered(lambda t: t.type == 'consu' or not t.invoice_policy).invoice_policy = 'order'
@api.model
def get_import_templates(self):
res = super(ProductTemplate, self).get_import_templates()
if self.env.context.get('sale_multi_pricelist_product_template'):
if self.user_has_groups('product.group_sale_pricelist'):
return [{
'label': _("Import Template for Products"),
'template': '/product/static/xls/product_template.xls'
}]
return res
@api.model
def _get_incompatible_types(self):
return []
@api.constrains(lambda self: self._get_incompatible_types())
def _check_incompatible_types(self):
incompatible_types = self._get_incompatible_types()
if len(incompatible_types) < 2:
return
fields = self.env['ir.model.fields'].sudo().search_read(
[('model', '=', 'product.template'), ('name', 'in', incompatible_types)],
['name', 'field_description'])
field_descriptions = {v['name']: v['field_description'] for v in fields}
field_list = incompatible_types + ['name']
values = self.read(field_list)
for val in values:
incompatible_fields = [f for f in incompatible_types if val[f]]
if len(incompatible_fields) > 1:
raise ValidationError(_(
"The product (%s) has incompatible values: %s",
val['name'],
','.join(field_descriptions[v] for v in incompatible_fields),
))

66
models/res_company.py Normal file
View File

@ -0,0 +1,66 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ResCompany(models.Model):
_inherit = 'res.company'
_check_company_auto = True
_sql_constraints = [
('check_quotation_validity_days',
'CHECK(quotation_validity_days >= 0)',
"You cannot set a negative number for the default quotation validity."
" Leave empty (or 0) to disable the automatic expiration of quotations."),
]
portal_confirmation_sign = fields.Boolean(string="Online Signature", default=True)
portal_confirmation_pay = fields.Boolean(string="Online Payment")
prepayment_percent = fields.Float(
string="Prepayment percentage",
default=1.0,
help="The percentage of the amount needed to be paid to confirm quotations.")
quotation_validity_days = fields.Integer(
string="Default Quotation Validity",
default=30,
help="Days between quotation proposal and expiration."
" 0 days means automatic expiration is disabled",
)
sale_discount_product_id = fields.Many2one(
comodel_name='product.product',
string="Discount Product",
domain=[
('type', '=', 'service'),
('invoice_policy', '=', 'order'),
],
help="Default product used for discounts",
check_company=True,
)
sale_down_payment_product_id = fields.Many2one(
comodel_name='product.product',
string="Deposit Product",
domain=[
('type', '=', 'service'),
('invoice_policy', '=', 'order'),
],
help="Default product used for down payments",
check_company=True,
)
# sale onboarding
sale_onboarding_payment_method = fields.Selection(
selection=[
('digital_signature', "Sign online"),
('paypal', "PayPal"),
('stripe', "Stripe"),
('other', "Pay with another payment provider"),
('manual', "Manual Payment"),
],
string="Sale onboarding selected payment method")
@api.constrains('prepayment_percent')
def _check_prepayment_percent(self):
for company in self:
if company.portal_confirmation_pay and not (0 < company.prepayment_percent <= 1.0):
raise ValidationError(_("Prepayment percentage must be a valid percentage."))

91
models/res_partner.py Normal file
View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.osv import expression
class ResPartner(models.Model):
_inherit = 'res.partner'
sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='Sale Order Count')
sale_order_ids = fields.One2many('sale.order', 'partner_id', 'Sales Order')
sale_warn = fields.Selection(WARNING_MESSAGE, 'Sales Warnings', default='no-message', help=WARNING_HELP)
sale_warn_msg = fields.Text('Message for Sales Order')
@api.model
def _get_sale_order_domain_count(self):
return []
def _compute_sale_order_count(self):
# retrieve all children partners and prefetch 'parent_id' on them
all_partners = self.with_context(active_test=False).search_fetch(
[('id', 'child_of', self.ids)],
['parent_id'],
)
sale_order_groups = self.env['sale.order']._read_group(
domain=expression.AND([self._get_sale_order_domain_count(), [('partner_id', 'in', all_partners.ids)]]),
groupby=['partner_id'], aggregates=['__count']
)
self_ids = set(self._ids)
self.sale_order_count = 0
for partner, count in sale_order_groups:
while partner:
if partner.id in self_ids:
partner.sale_order_count += count
partner = partner.parent_id
def _has_order(self, partner_domain):
self.ensure_one()
sale_order = self.env['sale.order'].sudo().search(
expression.AND([
partner_domain,
[
('state', 'in', ('sent', 'sale')),
]
]),
limit=1,
)
return bool(sale_order)
def _can_edit_name(self):
""" Can't edit `name` if there is (non draft) issued SO. """
return super()._can_edit_name() and not self._has_order(
[
('partner_invoice_id', '=', self.id),
('partner_id', '=', self.id),
]
)
def can_edit_vat(self):
""" Can't edit `vat` if there is (non draft) issued SO. """
return super().can_edit_vat() and not self._has_order(
[('partner_id', 'child_of', self.commercial_partner_id.id)]
)
def action_view_sale_order(self):
action = self.env['ir.actions.act_window']._for_xml_id('sale.act_res_partner_2_sale_order')
all_child = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
action["domain"] = [("partner_id", "in", all_child.ids)]
return action
def _compute_credit_to_invoice(self):
# EXTENDS 'account'
super()._compute_credit_to_invoice()
domain = [('partner_id', 'in', self.ids), ('state', '=', 'sale')]
group = self.env['sale.order']._read_group(domain, ['partner_id'], ['amount_to_invoice:sum'])
for partner, amount_to_invoice_sum in group:
partner.credit_to_invoice += amount_to_invoice_sum
def unlink(self):
# Unlink draft/cancelled SO so that the partner can be removed from database
self.env['sale.order'].sudo().search([
('state', 'in', ['draft', 'cancel']),
'|', '|',
('partner_id', 'in', self.ids),
('partner_invoice_id', 'in', self.ids),
('partner_shipping_id', 'in', self.ids),
]).unlink()
return super().unlink()

1915
models/sale_order.py Normal file

File diff suppressed because it is too large Load Diff

1270
models/sale_order_line.py Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More