diff --git a/README.md b/README.md index 0c6667bb6f2..a0158d919ee 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Odoo tutorials -This repository hosts the code for the bases and solutions of the -[official Odoo tutorials](https://www.odoo.com/documentation/16.0/developer/howtos.html). +This repository hosts the code for the bases of the modules used in the +[official Odoo tutorials](https://www.odoo.com/documentation/latest/developer/tutorials.html). -It has 2 branches for each Odoo version: one for the bases and one for -the solutions. For example, `16.0` and `16.0-solutions`. The first -contains the code of the modules that serve as base for the tutorials, -and the second contains the code of the same modules with the complete -solution. \ No newline at end of file +It has 3 branches for each Odoo version: one for the bases, one for the +[Discover the JS framework](https://www.odoo.com/documentation/latest/developer/tutorials/discover_js_framework.html) +tutorial's solutions, and one for the +[Master the Odoo web framework](https://www.odoo.com/documentation/latest/developer/tutorials/master_odoo_web_framework.html) +tutorial's solutions. For example, `17.0`, `17.0-discover-js-framework-solutions` and +`17.0-master-odoo-web-framework-solutions`. diff --git a/awesome_clicker/__init__.py b/awesome_clicker/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/awesome_clicker/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py new file mode 100644 index 00000000000..e57ef4d5bb0 --- /dev/null +++ b/awesome_clicker/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Clicker", + + 'summary': """ + Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" + """, + + 'description': """ + Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials/AwesomeClicker', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base', 'web'], + + 'data': [], + 'assets': { + 'web.assets_backend': [ + 'awesome_clicker/static/src/**/*', + ], + + }, + 'license': 'AGPL-3' +} diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py new file mode 100644 index 00000000000..b0f26a9a602 --- /dev/null +++ b/awesome_dashboard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py new file mode 100644 index 00000000000..31406e8addb --- /dev/null +++ b/awesome_dashboard/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Dashboard", + + 'summary': """ + Starting module for "Discover the JS framework, chapter 2: Build a dashboard" + """, + + 'description': """ + Starting module for "Discover the JS framework, chapter 2: Build a dashboard" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials/AwesomeDashboard', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base', 'web', 'mail', 'crm'], + + 'data': [ + 'views/views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'awesome_dashboard/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_dashboard/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py new file mode 100644 index 00000000000..56d4a051287 --- /dev/null +++ b/awesome_dashboard/controllers/controllers.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +import logging +import random + +from odoo import http +from odoo.http import request + +logger = logging.getLogger(__name__) + +class AwesomeDashboard(http.Controller): + @http.route('/awesome_dashboard/statistics', type='json', auth='user') + def get_statistics(self): + """ + Returns a dict of statistics about the orders: + 'average_quantity': the average number of t-shirts by order + 'average_time': the average time (in hours) elapsed between the + moment an order is created, and the moment is it sent + 'nb_cancelled_orders': the number of cancelled orders, this month + 'nb_new_orders': the number of new orders, this month + 'total_amount': the total amount of orders, this month + """ + + return { + 'average_quantity': random.randint(4, 12), + 'average_time': random.randint(4, 123), + 'nb_cancelled_orders': random.randint(0, 50), + 'nb_new_orders': random.randint(10, 200), + 'orders_by_size': { + 'm': random.randint(0, 150), + 's': random.randint(0, 150), + 'xl': random.randint(0, 150), + }, + 'total_amount': random.randint(100, 1000) + } + diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js new file mode 100644 index 00000000000..637fa4bb972 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.js @@ -0,0 +1,10 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml new file mode 100644 index 00000000000..1a2ac9a2fed --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.xml @@ -0,0 +1,8 @@ + + + + + hello dashboard + + + diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml new file mode 100644 index 00000000000..47fb2b6f258 --- /dev/null +++ b/awesome_dashboard/views/views.xml @@ -0,0 +1,11 @@ + + + + Dashboard + awesome_dashboard.dashboard + + + + + + diff --git a/awesome_gallery/__init__.py b/awesome_gallery/__init__.py new file mode 100644 index 00000000000..a0fdc10fe11 --- /dev/null +++ b/awesome_gallery/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/awesome_gallery/__manifest__.py b/awesome_gallery/__manifest__.py new file mode 100644 index 00000000000..624766dca89 --- /dev/null +++ b/awesome_gallery/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Gallery View", + 'summary': """ + Starting module for "Master the Odoo web framework, chapter 3: Create a Gallery View" + """, + + 'description': """ + Starting module for "Master the Odoo web framework, chapter 3: Create a Gallery View" + """, + + 'version': '0.1', + 'application': True, + 'category': 'Tutorials/AwesomeGallery', + 'installable': True, + 'depends': ['web', 'contacts'], + 'data': [ + 'views/views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'awesome_gallery/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_gallery/models/__init__.py b/awesome_gallery/models/__init__.py new file mode 100644 index 00000000000..7f0930ee744 --- /dev/null +++ b/awesome_gallery/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# import filename_python_file_within_folder_or_subfolder +from . import ir_action +from . import ir_ui_view diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py new file mode 100644 index 00000000000..eae20acbf5c --- /dev/null +++ b/awesome_gallery/models/ir_action.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class ActWindowView(models.Model): + _inherit = 'ir.actions.act_window.view' + + view_mode = fields.Selection(selection_add=[ + ('gallery', "Awesome Gallery") + ], ondelete={'gallery': 'cascade'}) diff --git a/awesome_gallery/models/ir_ui_view.py b/awesome_gallery/models/ir_ui_view.py new file mode 100644 index 00000000000..0c11b8298ac --- /dev/null +++ b/awesome_gallery/models/ir_ui_view.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class View(models.Model): + _inherit = 'ir.ui.view' + + type = fields.Selection(selection_add=[('gallery', "Awesome Gallery")]) diff --git a/awesome_gallery/static/src/gallery_view.js b/awesome_gallery/static/src/gallery_view.js new file mode 100644 index 00000000000..db904d1f478 --- /dev/null +++ b/awesome_gallery/static/src/gallery_view.js @@ -0,0 +1,3 @@ +/** @odoo-module */ + +// TODO: Begin here! diff --git a/awesome_gallery/views/views.xml b/awesome_gallery/views/views.xml new file mode 100644 index 00000000000..56327365875 --- /dev/null +++ b/awesome_gallery/views/views.xml @@ -0,0 +1,19 @@ + + + + + Contacts + res.partner + kanban,tree,form,activity + + {'default_is_company': True} + +

+ Create a Contact in your address book +

+ Odoo helps you track all activities related to your contacts. +

+
+
+
+
diff --git a/awesome_kanban/__init__.py b/awesome_kanban/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/awesome_kanban/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/awesome_kanban/__manifest__.py b/awesome_kanban/__manifest__.py new file mode 100644 index 00000000000..affef78bb12 --- /dev/null +++ b/awesome_kanban/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Kanban", + 'summary': """ + Starting module for "Master the Odoo web framework, chapter 4: Customize a kanban view" + """, + + 'description': """ + Starting module for "Master the Odoo web framework, chapter 4: Customize a kanban view. + """, + + 'version': '0.1', + 'application': True, + 'category': 'Tutorials/AwesomeKanban', + 'installable': True, + 'depends': ['web', 'crm'], + 'data': [ + 'views/views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'awesome_kanban/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_kanban/static/src/awesome_kanban_view.js b/awesome_kanban/static/src/awesome_kanban_view.js new file mode 100644 index 00000000000..9f33fc1300b --- /dev/null +++ b/awesome_kanban/static/src/awesome_kanban_view.js @@ -0,0 +1,3 @@ +/** @odoo-module */ + +// TODO: Define here your AwesomeKanban view diff --git a/awesome_kanban/views/views.xml b/awesome_kanban/views/views.xml new file mode 100644 index 00000000000..548b2907b6e --- /dev/null +++ b/awesome_kanban/views/views.xml @@ -0,0 +1,15 @@ + + + + + crm.lead.kanban.lead.awesome_gallery + crm.lead + + + + awesome_kanban + + + + + diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_owl/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py new file mode 100644 index 00000000000..77abad510ef --- /dev/null +++ b/awesome_owl/__manifest__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Owl", + + 'summary': """ + Starting module for "Discover the JS framework, chapter 1: Owl components" + """, + + 'description': """ + Starting module for "Discover the JS framework, chapter 1: Owl components" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Tutorials/AwesomeOwl', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base', 'web'], + 'application': True, + 'installable': True, + 'data': [ + 'views/templates.xml', + ], + 'assets': { + 'awesome_owl.assets_playground': [ + ('include', 'web._assets_helpers'), + 'web/static/src/scss/pre_variables.scss', + 'web/static/lib/bootstrap/scss/_variables.scss', + 'web/static/lib/bootstrap/scss/_maps.scss', + ('include', 'web._assets_bootstrap'), + ('include', 'web._assets_core'), + 'web/static/src/libs/fontawesome/css/font-awesome.css', + 'awesome_owl/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_owl/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py new file mode 100644 index 00000000000..bccfd6fe283 --- /dev/null +++ b/awesome_owl/controllers/controllers.py @@ -0,0 +1,10 @@ +from odoo import http +from odoo.http import request, route + +class OwlPlayground(http.Controller): + @http.route(['/awesome_owl'], type='http', auth='public') + def show_playground(self): + """ + Renders the owl playground page + """ + return request.render('awesome_owl.playground') diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js new file mode 100644 index 00000000000..1af6c827e0b --- /dev/null +++ b/awesome_owl/static/src/main.js @@ -0,0 +1,11 @@ +import { whenReady } from "@odoo/owl"; +import { mountComponent } from "@web/env"; +import { Playground } from "./playground"; + +const config = { + dev: true, + name: "Owl Tutorial", +}; + +// Mount the Playground component when the document.body is ready +whenReady(() => mountComponent(Playground, document.body, config)); diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js new file mode 100644 index 00000000000..657fb8b07bb --- /dev/null +++ b/awesome_owl/static/src/playground.js @@ -0,0 +1,7 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class Playground extends Component { + static template = "awesome_owl.playground"; +} diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml new file mode 100644 index 00000000000..4fb905d59f9 --- /dev/null +++ b/awesome_owl/static/src/playground.xml @@ -0,0 +1,10 @@ + + + + +
+ hello world +
+
+ +
diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml new file mode 100644 index 00000000000..aa54c1a7241 --- /dev/null +++ b/awesome_owl/views/templates.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/pos_workflow/__init__.py b/pos_workflow/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pos_workflow/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_workflow/__manifest__.py b/pos_workflow/__manifest__.py new file mode 100644 index 00000000000..c7f03f03b92 --- /dev/null +++ b/pos_workflow/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': "Pos Workflow", + + 'summary': """ + Module for creating picking from pos order + """, + + 'description': """ + Module for creating picking from pos order + """, + + 'category': 'Sales/Point of Sale', + 'version': '0.1', + + 'depends': ['point_of_sale', 'stock_account'], + 'application': True, + 'installable': True, + 'data': [], + 'assets': {'point_of_sale._assets_pos': [ + 'pos_workflow/static/src/**/*', + ], }, + 'license': 'AGPL-3' +} diff --git a/pos_workflow/models/__init__.py b/pos_workflow/models/__init__.py new file mode 100644 index 00000000000..e918923942f --- /dev/null +++ b/pos_workflow/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_picking +from . import pos_order diff --git a/pos_workflow/models/pos_order.py b/pos_workflow/models/pos_order.py new file mode 100644 index 00000000000..8a14411d2fa --- /dev/null +++ b/pos_workflow/models/pos_order.py @@ -0,0 +1,234 @@ +import logging +from datetime import datetime +from random import randrange +from pprint import pformat + +import psycopg2 + +from odoo import api, fields, models, tools, _ +from odoo.tools import float_is_zero +from odoo.exceptions import UserError, ValidationError +import base64 + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + state = fields.Selection(selection_add=[ + ('pay_later', 'Pay Later'), + ], ondelete={'posted': 'set default'}) + + @api.model + def _complete_values_from_session(self, session, values): + if values.get('state') and values['state'] in ('paid', 'pay_later') and not values.get('name'): + values['name'] = self._compute_order_name(session) + values.setdefault('pricelist_id', session.config_id.pricelist_id.id) + values.setdefault('fiscal_position_id', + session.config_id.default_fiscal_position_id.id) + values.setdefault('company_id', session.config_id.company_id.id) + return values + + @api.model + def sync_from_ui(self, orders): + """ Create and update Orders from the frontend PoS application. + + Create new orders and update orders that are in draft status. If an order already exists with a status + different from 'draft' it will be discarded, otherwise it will be saved to the database. If saved with + 'draft' status the order can be overwritten later by this function. + + :param orders: dictionary with the orders to be created. + :type orders: dict. + :param draft: Indicate if the orders are meant to be finalized or temporarily saved. + :type draft: bool. + :Returns: list -- list of db-ids for the created and updated orders. + """ + sync_token = randrange(100_000_000) # Use to differentiate 2 parallels calls to this function in the logs + _logger.info("PoS synchronisation #%d started for PoS orders references: %s", sync_token, [ + self._get_order_log_representation(order) for order in orders]) + order_ids = [] + for order in orders: + order_log_name = self._get_order_log_representation(order) + _logger.debug("PoS synchronisation #%d processing order %s order full data: %s", + sync_token, order_log_name, pformat(order)) + + if len(self._get_refunded_orders(order)) > 1: + raise ValidationError( + _('You can only refund products from the same order.')) + + existing_order = self._get_open_order(order) + if existing_order and existing_order.state in ('draft', 'pay_later'): + order_ids.append(self._process_order(order, existing_order)) + _logger.info("PoS synchronisation #%d order %s updated pos.order #%d", + sync_token, order_log_name, order_ids[-1]) + elif not existing_order: + order_ids.append(self._process_order(order, False)) + _logger.info("PoS synchronisation #%d order %s created pos.order #%d", + sync_token, order_log_name, order_ids[-1]) + else: + # In theory, this situation is unintended + # In practice it can happen when "Tip later" option is used + order_ids.append(existing_order.id) + _logger.info("PoS synchronisation #%d order %s sync ignored for existing PoS order %s (state: %s)", + sync_token, order_log_name, existing_order, existing_order.state) + + # Sometime pos_orders_ids can be empty. + pos_order_ids = self.env['pos.order'].browse(order_ids) + config_id = pos_order_ids.config_id.ids[0] if pos_order_ids else False + + for order in pos_order_ids: + order._ensure_access_token() + if not self.env.context.get('preparation'): + order.config_id.notify_synchronisation( + order.config_id.current_session_id.id, self.env.context.get('login_number', 0)) + + _logger.info("PoS synchronisation #%d finished", sync_token) + return pos_order_ids.read_pos_data(orders, config_id) + + @api.model + def _process_order(self, order, existing_order): + """Create or update an pos.order from a given dictionary. + + :param dict order: dictionary representing the order. + :param existing_order: order to be updated or False. + :type existing_order: pos.order. + :returns: id of created/updated pos.order + :rtype: int + """ + draft = True if order.get('state') == 'draft' else False + done = True if order.get('state') == 'pay_later' else False + pos_session = self.env['pos.session'].browse(order['session_id']) + if pos_session.state == 'closing_control' or pos_session.state == 'closed': + order['session_id'] = self._get_valid_session(order).id + + if order.get('partner_id'): + partner_id = self.env['res.partner'].browse(order['partner_id']) + if not partner_id.exists(): + order.update({ + "partner_id": False, + "to_invoice": False, + }) + + pos_order = False + combo_child_uuids_by_parent_uuid = self._prepare_combo_line_uuids( + order) + + pos_vals = {key: value for key, + value in order.items() if key != 'name'} + if not existing_order: + pos_order = self.create({ + **pos_vals, + 'pos_reference': order.get('name') + }) + pos_order = pos_order.with_company(pos_order.company_id) + else: + pos_order = existing_order + + # If the order is belonging to another session, it must be moved to the current session first + if order.get('session_id') and order['session_id'] != pos_order.session_id.id: + pos_order.write({'session_id': order['session_id']}) + + # Save lines and payments before to avoid exception if a line is deleted + # when vals change the state to 'paid' + for field in ['lines', 'payment_ids']: + if order.get(field): + existing_record_ids = self.env[pos_order[field]._name].browse( + [r[1] for r in order[field] if r[1] != 0]).exists().ids + existing_records_vals = [r for r in order[field] if r[0] not in [ + 1, 2, 3, 4] or r[1] in existing_record_ids] + pos_order.write({field: existing_records_vals}) + order[field] = [] + + del order['uuid'] + del order['access_token'] + pos_order.write(order) + + pos_order._link_combo_items(combo_child_uuids_by_parent_uuid) + self = self.with_company(pos_order.company_id) + self._process_payment_lines(order, pos_order, pos_session, draft, done) + return pos_order._process_saved_order(draft, done) + + def _process_payment_lines(self, pos_order, order, pos_session, draft, done): + """Create account.bank.statement.lines from the dictionary given to the parent function. + + If the payment_line is an updated version of an existing one, the existing payment_line will first be + removed before making a new one. + :param pos_order: dictionary representing the order. + :type pos_order: dict. + :param order: Order object the payment lines should belong to. + :type order: pos.order + :param pos_session: PoS session the order was created in. + :type pos_session: pos.session + :param draft: Indicate that the pos_order is not validated yet. + :type draft: bool. + """ + prec_acc = order.currency_id.decimal_places + + # Recompute amount paid because we don't trust the client + order.with_context(backend_recomputation=True).write( + {'amount_paid': sum(order.payment_ids.mapped('amount'))}) + + if not draft and not done and not float_is_zero(pos_order['amount_return'], prec_acc): + cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[ + :1] + if not cash_payment_method: + raise UserError( + _("No cash statement found for this session. Unable to record returned cash.")) + return_payment_vals = { + 'name': _('return'), + 'pos_order_id': order.id, + 'amount': -pos_order['amount_return'], + 'payment_date': fields.Datetime.now(), + 'payment_method_id': cash_payment_method.id, + 'is_change': True, + } + order.add_payment(return_payment_vals) + order._compute_prices() + + def _process_saved_order(self, draft, done): + self.ensure_one() + if not draft and self.state != 'cancel': + try: + self.action_pos_order_paid() + except psycopg2.DatabaseError: + # do not hide transactional errors, the order(s) won't be saved! + raise + except Exception as e: + _logger.error( + 'Could not fully process the POS Order: %s', tools.exception_to_unicode(e)) + self._create_order_picking(done) + self._compute_total_cost_in_real_time() + + if self.to_invoice and self.state == 'paid': + self._generate_pos_order_invoice() + + return self.id + + def _create_order_picking(self, done): + self.ensure_one() + ready_pickings = self.picking_ids.filtered( + lambda l: l.state not in ['cancel', 'done']) + picking_type = self.config_id.picking_type_id + if self.partner_id.property_stock_customer: + destination_id = self.partner_id.property_stock_customer.id + elif not picking_type or not picking_type.default_location_dest_id: + destination_id = self.env['stock.warehouse']._get_partner_locations()[ + 0].id + else: + destination_id = picking_type.default_location_dest_id.id + if self.shipping_date: + self.sudo().lines._launch_stock_rule_from_pos_order_lines() + elif self.picking_ids and ready_pickings: + pickings = self.env['stock.picking']._update_picking_from_pos_order_lines( + destination_id, self.lines, picking_type, ready_pickings[0], self.partner_id) + pickings.write({'pos_session_id': self.session_id.id, + 'pos_order_id': self.id, 'origin': self.name}) + else: + if self.picking_ids: + self.picking_ids.write({'state': 'cancel'}) + if self._should_create_picking_real_time(): + pickings = self.env['stock.picking']._create_picking_from_pos_order_lines( + destination_id, self.lines, picking_type, self.partner_id, done) + pickings.write({'pos_session_id': self.session_id.id, + 'pos_order_id': self.id, 'origin': self.name}) diff --git a/pos_workflow/models/stock_picking.py b/pos_workflow/models/stock_picking.py new file mode 100644 index 00000000000..409c61cd2d4 --- /dev/null +++ b/pos_workflow/models/stock_picking.py @@ -0,0 +1,96 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_is_zero + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + @api.model + def _create_picking_from_pos_order_lines(self, location_dest_id, lines, picking_type, partner=False, done=False): + """We'll create some picking based on order_lines""" + + pickings = self.env['stock.picking'] + stockable_lines = lines.filtered(lambda l: l.product_id.type == 'consu' and not float_is_zero( + l.qty, precision_rounding=l.product_id.uom_id.rounding)) + if not stockable_lines: + return pickings + positive_lines = stockable_lines.filtered(lambda l: l.qty > 0) + negative_lines = stockable_lines - positive_lines + + if positive_lines: + location_id = picking_type.default_location_src_id.id + positive_picking = self.env['stock.picking'].create( + self._prepare_picking_vals( + partner, picking_type, location_id, location_dest_id) + ) + + positive_picking._create_move_from_pos_order_lines(positive_lines) + self.env.flush_all() + try: + with self.env.cr.savepoint(): + if not done: + positive_picking._action_done() + except (UserError, ValidationError): + pass + + pickings |= positive_picking + if negative_lines: + if picking_type.return_picking_type_id: + return_picking_type = picking_type.return_picking_type_id + return_location_id = return_picking_type.default_location_dest_id.id + else: + return_picking_type = picking_type + return_location_id = picking_type.default_location_src_id.id + + negative_picking = self.env['stock.picking'].create( + self._prepare_picking_vals( + partner, return_picking_type, location_dest_id, return_location_id) + ) + negative_picking._create_move_from_pos_order_lines(negative_lines) + self.env.flush_all() + try: + with self.env.cr.savepoint(): + if not done: + negative_picking._action_done() + except (UserError, ValidationError): + pass + pickings |= negative_picking + return pickings + + @api.model + def _update_picking_from_pos_order_lines(self, location_dest_id, lines, picking_type, pickings, partner=False): + """We'll update some picking based on order_lines""" + + stockable_lines = lines.filtered(lambda l: l.product_id.type == 'consu' and not float_is_zero( + l.qty, precision_rounding=l.product_id.uom_id.rounding)) + if not stockable_lines: + return pickings + positive_lines = stockable_lines.filtered(lambda l: l.qty > 0) + negative_lines = stockable_lines - positive_lines + if positive_lines: + location_id = picking_type.default_location_src_id.id + pickings.move_ids.unlink() + pickings.write(self._prepare_picking_vals( + partner, picking_type, location_id, location_dest_id)) + + pickings._create_move_from_pos_order_lines(positive_lines) + self.env.flush_all() + pickings |= pickings + if negative_lines: + if picking_type.return_picking_type_id: + return_picking_type = picking_type.return_picking_type_id + return_location_id = return_picking_type.default_location_dest_id.id + else: + return_picking_type = picking_type + return_location_id = picking_type.default_location_src_id.id + + pickings.move_ids.unlink() + pickings.write( + self._prepare_picking_vals( + partner, return_picking_type, location_dest_id, return_location_id) + ) + pickings._create_move_from_pos_order_lines(negative_lines) + self.env.flush_all() + pickings |= pickings + return pickings diff --git a/pos_workflow/static/src/pos_order.js b/pos_workflow/static/src/pos_order.js new file mode 100644 index 00000000000..60fd14fe096 --- /dev/null +++ b/pos_workflow/static/src/pos_order.js @@ -0,0 +1,8 @@ +import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { patch } from "@web/core/utils/patch"; + +patch(PosOrder.prototype, { + get finalized() { + return !["draft", "pay_later"].includes(this.state); + } +}); diff --git a/pos_workflow/static/src/pos_store.js b/pos_workflow/static/src/pos_store.js new file mode 100644 index 00000000000..5e3ac5b54e3 --- /dev/null +++ b/pos_workflow/static/src/pos_store.js @@ -0,0 +1,12 @@ +import { patch } from "@web/core/utils/patch"; +import { PosStore } from "@point_of_sale/app/store/pos_store"; + +patch(PosStore.prototype, { + add_new_order() { + const currentOrder = this.get_order(); + if (currentOrder?.state === "pay_later") { + return currentOrder; + } + return super.add_new_order(...arguments); + } +}); diff --git a/pos_workflow/static/src/product_screen.js b/pos_workflow/static/src/product_screen.js new file mode 100644 index 00000000000..87f839757b8 --- /dev/null +++ b/pos_workflow/static/src/product_screen.js @@ -0,0 +1,30 @@ +import { _t } from "@web/core/l10n/translation"; +import { onWillRender } from "@odoo/owl"; +import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; +import { patch } from "@web/core/utils/patch"; +import { ask } from "@point_of_sale/app/store/make_awaitable_dialog"; + +patch(ProductScreen.prototype, { + async createOrderPosted() { + if ( + !this.currentOrder.get_partner() + ) { + const confirmed = await ask(this.dialog, { + title: _t("Please select the Customer"), + body: _t( + "You need to select the customer before you can invoice or ship an order." + ), + }); + return false; + } + this.currentOrder.state = "pay_later"; + this.pos.addPendingOrder([this.currentOrder.id]); + let syncOrderResult; + try { + syncOrderResult = await this.pos.syncAllOrders({ throw: true }); + } + catch (error){ + console.error("Sync failed", error); + } + } +}); diff --git a/pos_workflow/static/src/product_screen.xml b/pos_workflow/static/src/product_screen.xml new file mode 100644 index 00000000000..7962a7f3f89 --- /dev/null +++ b/pos_workflow/static/src/product_screen.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/website_airproof/README.md b/website_airproof/README.md new file mode 100644 index 00000000000..6c6390b9424 --- /dev/null +++ b/website_airproof/README.md @@ -0,0 +1,18 @@ +# Odoo Tutorial : Build a website theme + +This branch contains the code necessary for the creation of the website for our Airproof example. +Example used to illustrate the exercises given in the Odoo tutorial: Build a website theme. + +Here is the final design of the 4 pages of Airproof that will be created throughout this tutorial. + +**Home** +![Airproof home page](airproof-home-page.jpg) + +**Contact page** +![Airproof contact page](airproof-contact-page.jpg) + +**Shop page** +![Airproof shop page](airproof-shop-page.jpg) + +**Product page** +![Airproof product page](airproof-product-page.jpg) diff --git a/website_airproof/__init__.py b/website_airproof/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/website_airproof/__manifest__.py b/website_airproof/__manifest__.py new file mode 100644 index 00000000000..f6cd9dc0d5e --- /dev/null +++ b/website_airproof/__manifest__.py @@ -0,0 +1,59 @@ +{ + 'name': 'Airproof Theme', + 'description': 'Airproof Theme - Drones, modelling, camera', + 'category': 'Website/Theme', + # 'version': '18.0.1.0', + 'author': 'PSBE Designers', + 'license': 'LGPL-3', + 'depends': ['website_sale', 'website_sale_wishlist', 'website_blog', 'website_mass_mailing'], + 'data': [ + # Options + 'data/presets.xml', + 'data/website.xml', + # Menu + 'data/menu.xml', + # Gradients + 'data/gradients.xml', + # Shapes + 'data/shapes.xml', + # Pages + 'data/pages/home.xml', + 'data/pages/contact.xml', + # Frontend + 'views/new_page_template_templates.xml', + 'views/website_templates.xml', + 'views/website_sale_templates.xml', + 'views/website_sale_wishlist_templates.xml', + # Snippets + 'views/snippets/options.xml', + 'views/snippets/s_airproof_carousel.xml', + # Images + 'data/images.xml', + ], + 'assets': { + 'web._assets_primary_variables': [ + 'website_airproof/static/src/scss/primary_variables.scss', + ], + 'web._assets_frontend_helpers': [ + ('prepend', 'website_airproof/static/src/scss/bootstrap_overridden.scss'), + ], + 'web.assets_frontend': [ + # SCSS + 'website_airproof/static/src/scss/font.scss', + 'website_airproof/static/src/scss/components/mouse_follower.scss', + 'website_airproof/static/src/scss/layout/header.scss', + 'website_airproof/static/src/scss/pages/product_page.scss', + 'website_airproof/static/src/scss/pages/shop.scss', + 'website_airproof/static/src/scss/snippets/caroussel.scss', + 'website_airproof/static/src/scss/snippets/newsletter.scss', + 'website_airproof/static/src/snippets/s_airproof_carousel/000.scss', + # JS + 'website_airproof/static/src/js/mouse_follower.js', + ], + }, + 'new_page_templates': { + 'airproof': { + 'services': ['s_parallax', 's_airproof_key_benefits_h2', 's_call_to_action', 's_airproof_carousel'] + } + }, +} diff --git a/website_airproof/airproof-contact-page.jpg b/website_airproof/airproof-contact-page.jpg new file mode 100644 index 00000000000..cc1008a28cb Binary files /dev/null and b/website_airproof/airproof-contact-page.jpg differ diff --git a/website_airproof/airproof-home-page.jpg b/website_airproof/airproof-home-page.jpg new file mode 100644 index 00000000000..53eb1d617b2 Binary files /dev/null and b/website_airproof/airproof-home-page.jpg differ diff --git a/website_airproof/airproof-product-page.jpg b/website_airproof/airproof-product-page.jpg new file mode 100644 index 00000000000..bf07f58651a Binary files /dev/null and b/website_airproof/airproof-product-page.jpg differ diff --git a/website_airproof/airproof-shop-page.jpg b/website_airproof/airproof-shop-page.jpg new file mode 100644 index 00000000000..c77e724d7f1 Binary files /dev/null and b/website_airproof/airproof-shop-page.jpg differ diff --git a/website_airproof/data/gradients.xml b/website_airproof/data/gradients.xml new file mode 100644 index 00000000000..fd68f07b17c --- /dev/null +++ b/website_airproof/data/gradients.xml @@ -0,0 +1,14 @@ + + + + website_airproof.colorpicker + Custom Gradients + qweb + + + + + + + + diff --git a/website_airproof/data/images.xml b/website_airproof/data/images.xml new file mode 100644 index 00000000000..e3c7414a4d0 --- /dev/null +++ b/website_airproof/data/images.xml @@ -0,0 +1,188 @@ + + + + + + + + + White arrow icon + + ir.ui.view + + + + Glasses icon + + ir.ui.view + + + + 4K icon + + ir.ui.view + + + + Hand with drone icon + + ir.ui.view + + + + Control icon + + ir.ui.view + + + + Shopping icon + + ir.ui.view + + + + Phone icon + + ir.ui.view + + + + Envelop icon + + ir.ui.view + + + + Arrow icon + + ir.ui.view + + + + Small arrow icon + + ir.ui.view + + + + Check icon + + ir.ui.view + + + + Black Phone icon + + ir.ui.view + + + + Black Envelop icon + + ir.ui.view + + + + + + Drone Airproof Mini + + ir.ui.view + + + + Drone Airproof Pro + + ir.ui.view + + + + Drone Airproof Robin + + ir.ui.view + + + + Drone Airproof Falcon + + ir.ui.view + + + + Drone Airproof Eagle + + ir.ui.view + + + + + + + Drone accessories picture + + ir.ui.view + + + + Drone Robin picture + + ir.ui.view + + + + Drone school picture + + ir.ui.view + + + + Drone flying picture + + ir.ui.view + + + + + + Fields topview + + ir.ui.view + + + + + + Highway topview + + ir.ui.view + + + + + + Drone with blue background picture + + ir.ui.view + + + + + + Sticker logo + + ir.ui.view + + + + Drone picture + + ir.ui.view + + + diff --git a/website_airproof/data/menu.xml b/website_airproof/data/menu.xml new file mode 100644 index 00000000000..ee0a9319f19 --- /dev/null +++ b/website_airproof/data/menu.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + Waterproof drones + + 1 + 10 + +
+ +
+
+
+ + + + About us + /about-us + 1 + + 20 + + + + + Blog + + 1 + 30 + + + Our latest news + /blog/our-latest-news-2 + 1 + + 31 + + + Tutorials + /blog/tutorials-3 + 1 + + 32 + + + + + Contact us + /contactus + 1 + + 40 + +
diff --git a/website_airproof/data/pages/contact.xml b/website_airproof/data/pages/contact.xml new file mode 100644 index 00000000000..f6a4502dc75 --- /dev/null +++ b/website_airproof/data/pages/contact.xml @@ -0,0 +1,156 @@ + + + + + + + + + Contact us + + website_airproof.page_contact + /contactus + + qweb + + + + + Contact us + + + +