[IMP] point_of_sale,pos_sale: Sale Order integration
Since now, there was no possibilities to pay a Sale Order in a point of sale without re-encoding every lines of the sale order in a pos order. So we've now added the possibility to choose a sale order from the point of sale, and apply a down payment or settle the selected order. You still get the possibility to invoice it, and the stock is correctly managed. The SO is then updated accordingly to what have been done throught the POS. closes odoo/odoo#74096 Task-id: 2500902 Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
@@ -64,15 +64,15 @@ odoo.define('point_of_sale.DebugWidget', function (require) {
|
||||
this.state.weightInput = '';
|
||||
this.env.pos.proxy.debug_reset_weight();
|
||||
}
|
||||
barcodeScan() {
|
||||
this.env.pos.barcode_reader.scan(this.state.barcodeInput);
|
||||
async barcodeScan() {
|
||||
await this.env.pos.barcode_reader.scan(this.state.barcodeInput);
|
||||
}
|
||||
barcodeScanEAN() {
|
||||
async barcodeScanEAN() {
|
||||
const ean = this.env.pos.barcode_reader.barcode_parser.sanitize_ean(
|
||||
this.state.barcodeInput || '0'
|
||||
);
|
||||
this.state.barcodeInput = ean;
|
||||
this.env.pos.barcode_reader.scan(ean);
|
||||
await this.env.pos.barcode_reader.scan(ean);
|
||||
}
|
||||
async deleteOrders() {
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
|
||||
@@ -8,7 +8,8 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { onChangeOrder, useBarcodeReader } = require('point_of_sale.custom_hooks');
|
||||
const { Gui } = require('point_of_sale.Gui');
|
||||
const { useState } = owl.hooks;
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
const { useState, onMounted } = owl.hooks;
|
||||
const { parse } = require('web.field_utils');
|
||||
|
||||
class ProductScreen extends ControlButtonsMixin(PosComponent) {
|
||||
@@ -34,6 +35,10 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
|
||||
triggerAtInput: 'update-selected-orderline',
|
||||
useWithBarcode: true,
|
||||
});
|
||||
// Call `reset` when the `onMounted` callback in `NumberBuffer.use` is done.
|
||||
// We don't do this in the `mounted` lifecycle method because it is called before
|
||||
// the callbacks in `onMounted` hook.
|
||||
onMounted(() => NumberBuffer.reset());
|
||||
this.state = useState({ numpadMode: 'quantity' });
|
||||
this.mobile_pane = this.props.mobile_pane || 'right';
|
||||
}
|
||||
@@ -184,6 +189,7 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
|
||||
}
|
||||
async _newOrderlineSelected() {
|
||||
NumberBuffer.reset();
|
||||
this.state.numpadMode = 'quantity';
|
||||
}
|
||||
_setValue(val) {
|
||||
if (this.currentOrder.get_selected_orderline()) {
|
||||
@@ -202,9 +208,34 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
|
||||
}
|
||||
}
|
||||
async _barcodeProductAction(code) {
|
||||
const product = this.env.pos.db.get_product_by_barcode(code.base_code)
|
||||
let product = this.env.pos.db.get_product_by_barcode(code.base_code);
|
||||
if (!product) {
|
||||
return this._barcodeErrorAction(code);
|
||||
// find the barcode in the backend
|
||||
let foundProductIds = [];
|
||||
try {
|
||||
foundProductIds = await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'search',
|
||||
args: [[['barcode', '=', code.base_code]]],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isRpcError(error) && error.message.code < 0) {
|
||||
return this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (foundProductIds.length) {
|
||||
await this.env.pos._addProducts(foundProductIds);
|
||||
// assume that the result is unique.
|
||||
product = this.env.pos.db.get_product_by_id(foundProductIds[0]);
|
||||
} else {
|
||||
return this._barcodeErrorAction(code);
|
||||
}
|
||||
}
|
||||
const options = await this._getAddProductOptions(product);
|
||||
// Do not proceed on adding the product when no options is returned.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
odoo.define('point_of_sale.BarcodeReader', function (require) {
|
||||
"use strict";
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var Mutex = concurrency.Mutex;
|
||||
|
||||
// this module interfaces with the barcode reader. It assumes the barcode reader
|
||||
// is set-up to act like a keyboard. Use connect() and disconnect() to activate
|
||||
@@ -15,6 +17,7 @@ var BarcodeReader = core.Class.extend({
|
||||
],
|
||||
|
||||
init: function (attributes) {
|
||||
this.mutex = new Mutex();
|
||||
this.pos = attributes.pos;
|
||||
this.action_callbacks = {};
|
||||
this.exclusive_callbacks = {};
|
||||
@@ -27,7 +30,10 @@ var BarcodeReader = core.Class.extend({
|
||||
this.action_callback_stack = [];
|
||||
|
||||
core.bus.on('barcode_scanned', this, function (barcode) {
|
||||
this.scan(barcode);
|
||||
// use mutex to make sure scans are done one after the other
|
||||
this.mutex.exec(async () => {
|
||||
await this.scan(barcode);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -98,7 +104,7 @@ var BarcodeReader = core.Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
scan: function (code) {
|
||||
scan: async function (code) {
|
||||
if (!code) return;
|
||||
|
||||
const callbacks = Object.keys(this.exclusive_callbacks).length
|
||||
@@ -110,7 +116,9 @@ var BarcodeReader = core.Class.extend({
|
||||
}
|
||||
for (const parsed_result of parsed_results) {
|
||||
if (callbacks[parsed_result.type]) {
|
||||
[...callbacks[parsed_result.type]].map(cb => cb(parsed_result));
|
||||
for (const cb of callbacks[parsed_result.type]) {
|
||||
await cb(parsed_result);
|
||||
}
|
||||
} else if (callbacks.error) {
|
||||
[...callbacks.error].map(cb => cb(parsed_result));
|
||||
} else {
|
||||
|
||||
@@ -1162,26 +1162,6 @@ exports.PosModel = Backbone.Model.extend({
|
||||
return server_ids;
|
||||
},
|
||||
|
||||
scan_product: function(parsed_code){
|
||||
var selectedOrder = this.get_order();
|
||||
var product = this.db.get_product_by_barcode(parsed_code.base_code);
|
||||
|
||||
if(!product){
|
||||
return false;
|
||||
}
|
||||
|
||||
if(parsed_code.type === 'price'){
|
||||
selectedOrder.add_product(product, {price:parsed_code.value});
|
||||
}else if(parsed_code.type === 'weight'){
|
||||
selectedOrder.add_product(product, {quantity:parsed_code.value, merge:false});
|
||||
}else if(parsed_code.type === 'discount'){
|
||||
selectedOrder.add_product(product, {discount:parsed_code.value, merge:false});
|
||||
}else{
|
||||
selectedOrder.add_product(product);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Exports the paid orders (the ones waiting for internet connection)
|
||||
export_paid_orders: function() {
|
||||
return JSON.stringify({
|
||||
@@ -1390,6 +1370,26 @@ exports.PosModel = Backbone.Model.extend({
|
||||
getCurrencySymbol() {
|
||||
return this.currency ? this.currency.symbol : '$';
|
||||
},
|
||||
/**
|
||||
* Make the products corresponding to the given ids to be available_in_pos and
|
||||
* fetch them to be added on the loaded products.
|
||||
*/
|
||||
async _addProducts(ids){
|
||||
await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'write',
|
||||
args: [ids, {'available_in_pos': true}],
|
||||
context: this.session.user_context,
|
||||
});
|
||||
let product_model = _.find(this.models, (model) => model.model === 'product.product');
|
||||
let product = await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'read',
|
||||
args: [ids, product_model.fields],
|
||||
context: { ...this.session.user_context, ...product_model.context() },
|
||||
});
|
||||
product_model.loaded(this, product);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,10 +17,30 @@ This module adds a custom Sales Team for the Point of Sale. This enables you to
|
||||
'data/pos_sale_data.xml',
|
||||
'security/pos_sale_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/point_of_sale_report.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/pos_order_views.xml',
|
||||
'views/sales_team_views.xml',
|
||||
'views/pos_config_views.xml',
|
||||
'views/stock_template.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'point_of_sale.assets': [
|
||||
'pos_sale/static/src/css/pos_sale.css',
|
||||
'pos_sale/static/src/js/models.js',
|
||||
'pos_sale/static/src/js/SetSaleOrderButton.js',
|
||||
'pos_sale/static/src/js/OrderManagementScreen/SaleOrderFetcher.js',
|
||||
'pos_sale/static/src/js/OrderManagementScreen/SaleOrderList.js',
|
||||
'pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementControlPanel.js',
|
||||
'pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js',
|
||||
'pos_sale/static/src/js/OrderManagementScreen/SaleOrderRow.js',
|
||||
'pos_sale/static/src/js/ProductScreen.js',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'pos_sale/static/src/xml/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
<record id="sales_team.pos_sales_team" model="crm.team">
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="pos_sale.default_downpayment_product" model="product.product">
|
||||
<field name="name">Down Payment (POS)</field>
|
||||
<field name="available_in_pos">False</field>
|
||||
<field name="standard_price">0.00</field>
|
||||
<field name="list_price">0.00</field>
|
||||
<field name="weight">0.00</field>
|
||||
<field name="type">service</field>
|
||||
<field name="taxes_id" eval="[(5,)]"/>
|
||||
<field name="categ_id" ref="point_of_sale.product_category_pos"/>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="purchase_ok">False</field>
|
||||
</record>
|
||||
|
||||
<record model="pos.config" id="point_of_sale.pos_config_main">
|
||||
<field name="down_payment_product_id" ref="pos_sale.default_downpayment_product"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -5,3 +5,4 @@ from . import pos_config
|
||||
from . import pos_order
|
||||
from . import crm_team
|
||||
from . import pos_session
|
||||
from . import sale_order
|
||||
|
||||
@@ -12,6 +12,9 @@ class PosConfig(models.Model):
|
||||
crm_team_id = fields.Many2one(
|
||||
'crm.team', string="Sales Team", ondelete="set null",
|
||||
help="This Point of sale's sales will be related to this Sales Team.")
|
||||
down_payment_product_id = fields.Many2one('product.product',
|
||||
string="Down Payment Product",
|
||||
help="This product will be used as down payment on a sale order.")
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _get_default_pos_team(self):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class PosOrder(models.Model):
|
||||
@@ -9,6 +10,11 @@ class PosOrder(models.Model):
|
||||
|
||||
currency_rate = fields.Float(compute='_compute_currency_rate', store=True, digits=0, readonly=True)
|
||||
crm_team_id = fields.Many2one('crm.team', string="Sales Team", ondelete="set null")
|
||||
sale_order_count = fields.Integer(string='Sale Order Count', compute='_count_sale_order', readonly=True, groups="sales_team.group_sale_salesman")
|
||||
|
||||
def _count_sale_order(self):
|
||||
for order in self:
|
||||
order.sale_order_count = len(order.lines.mapped('sale_order_origin_id'))
|
||||
|
||||
@api.model
|
||||
def _complete_values_from_session(self, session, values):
|
||||
@@ -26,3 +32,91 @@ class PosOrder(models.Model):
|
||||
invoice_vals = super(PosOrder, self)._prepare_invoice_vals()
|
||||
invoice_vals['team_id'] = self.crm_team_id
|
||||
return invoice_vals
|
||||
|
||||
@api.model
|
||||
def create_from_ui(self, orders, draft=False):
|
||||
order_ids = super(PosOrder, self).create_from_ui(orders, draft)
|
||||
for order in self.sudo().browse([o['id'] for o in order_ids]):
|
||||
for line in order.lines.filtered(lambda l: l.product_id == order.config_id.down_payment_product_id and l.qty > 0 and l.sale_order_origin_id):
|
||||
sale_lines = line.sale_order_origin_id.order_line
|
||||
sale_line = self.env['sale.order.line'].create({
|
||||
'order_id': line.sale_order_origin_id.id,
|
||||
'product_id': line.product_id.id,
|
||||
'price_unit': line.price_unit,
|
||||
'product_uom_qty': 0,
|
||||
'tax_id': [(6, 0, line.tax_ids.ids)],
|
||||
'is_downpayment': True,
|
||||
'discount': line.discount,
|
||||
'sequence': sale_lines and sale_lines[-1].sequence + 1 or 10,
|
||||
})
|
||||
sale_line._compute_tax_id()
|
||||
line.sale_order_line_id = sale_line
|
||||
|
||||
so_lines = order.lines.mapped('sale_order_line_id')
|
||||
|
||||
# confirm the unconfirmed sale orders that are linked to the sale order lines
|
||||
sale_orders = so_lines.mapped('order_id')
|
||||
for sale_order in sale_orders.filtered(lambda so: so.state in ['draft', 'sent']):
|
||||
sale_order.action_confirm()
|
||||
|
||||
# update the demand qty in the stock moves related to the sale order line
|
||||
# flush the qty_delivered to make sure the updated qty_delivered is used when
|
||||
# updating the demand value
|
||||
so_lines.flush(['qty_delivered'])
|
||||
# track the waiting pickings
|
||||
waiting_picking_ids = set()
|
||||
for so_line in so_lines:
|
||||
for stock_move in so_line.move_ids:
|
||||
picking = stock_move.picking_id
|
||||
if not picking.state in ['waiting', 'confirmed', 'assigned']:
|
||||
continue
|
||||
new_qty = so_line.product_uom_qty - so_line.qty_delivered
|
||||
if float_compare(new_qty, 0, precision_rounding=stock_move.product_uom.rounding) <= 0:
|
||||
new_qty = 0
|
||||
stock_move.product_uom_qty = so_line.product_uom._compute_quantity(new_qty, stock_move.product_uom, False)
|
||||
waiting_picking_ids.add(picking.id)
|
||||
|
||||
def is_product_uom_qty_zero(move):
|
||||
return float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding)
|
||||
|
||||
# cancel the waiting pickings if each product_uom_qty of move is zero
|
||||
for picking in self.env['stock.picking'].browse(waiting_picking_ids):
|
||||
if all(is_product_uom_qty_zero(move) for move in picking.move_lines):
|
||||
picking.action_cancel()
|
||||
|
||||
return order_ids
|
||||
|
||||
def action_view_sale_order(self):
|
||||
self.ensure_one()
|
||||
linked_orders = self.lines.mapped('sale_order_origin_id')
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Linked Sale Orders'),
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', linked_orders.ids)],
|
||||
}
|
||||
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
|
||||
sale_order_origin_id = fields.Many2one('sale.order', string="Linked Sale Order")
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string="Source Sale Order Line")
|
||||
down_payment_details = fields.Text(string="Down Payment Details")
|
||||
|
||||
def _export_for_ui(self, orderline):
|
||||
result = super()._export_for_ui(orderline)
|
||||
# NOTE We are not exporting 'sale_order_line_id' because it is being used in any views in the POS App.
|
||||
result['down_payment_details'] = bool(orderline.down_payment_details) and orderline.down_payment_details
|
||||
result['sale_order_origin_id'] = bool(orderline.sale_order_origin_id) and orderline.sale_order_origin_id.read(fields=['name'])[0]
|
||||
return result
|
||||
|
||||
def _order_line_fields(self, line, session_id):
|
||||
result = super()._order_line_fields(line, session_id)
|
||||
vals = result[2]
|
||||
if vals.get('sale_order_origin_id', False):
|
||||
vals['sale_order_origin_id'] = vals['sale_order_origin_id']['id']
|
||||
if vals.get('sale_order_line_id', False):
|
||||
vals['sale_order_line_id'] = vals['sale_order_line_id']['id']
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
pos_order_line_ids = fields.One2many('pos.order.line', 'sale_order_origin_id', string="Order lines Transfered to Point of Sale", readonly=True, groups="point_of_sale.group_pos_user")
|
||||
pos_order_count = fields.Integer(string='Pos Order Count', compute='_count_pos_order', readonly=True, groups="point_of_sale.group_pos_user")
|
||||
|
||||
def _count_pos_order(self):
|
||||
for order in self:
|
||||
linked_orders = order.pos_order_line_ids.mapped('order_id')
|
||||
order.pos_order_count = len(linked_orders)
|
||||
|
||||
def action_view_pos_order(self):
|
||||
self.ensure_one()
|
||||
linked_orders = self.pos_order_line_ids.mapped('order_id')
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Linked POS Orders'),
|
||||
'res_model': 'pos.order',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', linked_orders.ids)],
|
||||
}
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
pos_order_line_ids = fields.One2many('pos.order.line', 'sale_order_line_id', string="Order lines Transfered to Point of Sale", readonly=True, groups="point_of_sale.group_pos_user")
|
||||
|
||||
@api.depends('pos_order_line_ids.qty')
|
||||
def _compute_qty_delivered(self):
|
||||
super()._compute_qty_delivered()
|
||||
for sale_line in self:
|
||||
sale_line.qty_delivered += sum([self._convert_qty(sale_line, pos_line.qty, 'p2s') for pos_line in sale_line.pos_order_line_ids if sale_line.product_id.type != 'service'], 0)
|
||||
|
||||
@api.depends('pos_order_line_ids.qty')
|
||||
def _get_invoice_qty(self):
|
||||
super()._get_invoice_qty()
|
||||
for sale_line in self:
|
||||
sale_line.qty_invoiced += sum([self._convert_qty(sale_line, pos_line.qty, 'p2s') for pos_line in sale_line.pos_order_line_ids], 0)
|
||||
|
||||
def read_converted(self):
|
||||
field_names = ["product_id", "price_unit", "product_uom_qty", "tax_id", "qty_delivered", "qty_invoiced", "discount", "qty_to_invoice", "price_total"]
|
||||
results = []
|
||||
for sale_line in self:
|
||||
if sale_line.product_type:
|
||||
product_uom = sale_line.product_id.uom_id
|
||||
sale_line_uom = sale_line.product_uom
|
||||
item = sale_line.read(field_names)[0]
|
||||
if sale_line.product_id.tracking != 'none':
|
||||
item['lot_names'] = sale_line.move_ids.move_line_ids.lot_id.mapped('name')
|
||||
if product_uom == sale_line_uom:
|
||||
results.append(item)
|
||||
continue
|
||||
item['product_uom_qty'] = self._convert_qty(sale_line, item['product_uom_qty'], 's2p')
|
||||
item['qty_delivered'] = self._convert_qty(sale_line, item['qty_delivered'], 's2p')
|
||||
item['qty_invoiced'] = self._convert_qty(sale_line, item['qty_invoiced'], 's2p')
|
||||
item['qty_to_invoice'] = self._convert_qty(sale_line, item['qty_to_invoice'], 's2p')
|
||||
item['price_unit'] = sale_line_uom._compute_price(item['price_unit'], product_uom)
|
||||
results.append(item)
|
||||
|
||||
elif sale_line.display_type == 'line_note':
|
||||
if results:
|
||||
results[-1]['customer_note'] = sale_line.name
|
||||
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _convert_qty(self, sale_line, qty, direction):
|
||||
"""Converts the given QTY based on the given SALE_LINE and DIR.
|
||||
|
||||
if DIR='s2p': convert from sale line uom to product uom
|
||||
if DIR='p2s': convert from product uom to sale line uom
|
||||
"""
|
||||
product_uom = sale_line.product_id.uom_id
|
||||
sale_line_uom = sale_line.product_uom
|
||||
if direction == 's2p':
|
||||
return sale_line_uom._compute_quantity(qty, product_uom, False)
|
||||
elif direction == 'p2s':
|
||||
return product_uom._compute_quantity(qty, sale_line_uom, False)
|
||||
@@ -79,6 +79,10 @@ class SaleReport(models.Model):
|
||||
'''
|
||||
return from_
|
||||
|
||||
def _where_pos(self):
|
||||
where_ = 'l.sale_order_line_id is NULL'
|
||||
return where_
|
||||
|
||||
def _group_by_pos(self):
|
||||
groupby_ = '''
|
||||
l.order_id,
|
||||
@@ -108,7 +112,7 @@ class SaleReport(models.Model):
|
||||
if not fields:
|
||||
fields = {}
|
||||
res = super()._query(with_clause, fields, groupby, from_clause)
|
||||
current = '(SELECT %s FROM %s GROUP BY %s)' % \
|
||||
(self._select_pos(fields), self._from_pos(), self._group_by_pos())
|
||||
current = '(SELECT %s FROM %s WHERE %s GROUP BY %s)' % \
|
||||
(self._select_pos(fields), self._from_pos(), self._where_pos(), self._group_by_pos())
|
||||
|
||||
return '%s UNION ALL %s' % (res, current)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.sale-order-info td {
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
odoo.define('pos_sale.SaleOrderFetcher', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { EventBus } = owl.core;
|
||||
const { Gui } = require('point_of_sale.Gui');
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
const models = require('point_of_sale.models');
|
||||
|
||||
class SaleOrderFetcher extends EventBus {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentPage = 1;
|
||||
this.ordersToShow = [];
|
||||
this.totalCount = 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* for nPerPage = 10
|
||||
* +--------+----------+
|
||||
* | nItems | lastPage |
|
||||
* +--------+----------+
|
||||
* | 2 | 1 |
|
||||
* | 10 | 1 |
|
||||
* | 11 | 2 |
|
||||
* | 30 | 3 |
|
||||
* | 35 | 4 |
|
||||
* +--------+----------+
|
||||
*/
|
||||
get lastPage() {
|
||||
const nItems = this.totalCount;
|
||||
return Math.trunc(nItems / (this.nPerPage + 1)) + 1;
|
||||
}
|
||||
/**
|
||||
* Calling this methods populates the `ordersToShow` then trigger `update` event.
|
||||
* @related get
|
||||
*
|
||||
* NOTE: This is tightly-coupled with pagination. So if the current page contains all
|
||||
* active orders, it will not fetch anything from the server but only sets `ordersToShow`
|
||||
* to the active orders that fits the current page.
|
||||
*/
|
||||
async fetch() {
|
||||
try {
|
||||
let limit, offset;
|
||||
let start, end;
|
||||
// Show orders from the backend.
|
||||
offset =
|
||||
this.nPerPage +
|
||||
(this.currentPage - 1 - 1) *
|
||||
this.nPerPage;
|
||||
limit = this.nPerPage;
|
||||
this.ordersToShow = await this._fetch(limit, offset);
|
||||
|
||||
this.trigger('update');
|
||||
} catch (error) {
|
||||
if (isRpcError(error) && error.message.code < 0) {
|
||||
Gui.showPopup('ErrorPopup', {
|
||||
title: this.comp.env._t('Network Error'),
|
||||
body: this.comp.env._t('Unable to fetch orders if offline.'),
|
||||
});
|
||||
Gui.setSyncStatus('error');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This returns the orders from the backend that needs to be shown.
|
||||
* If the order is already in cache, the full information about that
|
||||
* order is not fetched anymore, instead, we use info from cache.
|
||||
*
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
*/
|
||||
async _fetch(limit, offset) {
|
||||
const sale_orders = await this._getOrderIdsForCurrentPage(limit, offset);
|
||||
|
||||
this.totalCount = sale_orders.length;
|
||||
return sale_orders;
|
||||
}
|
||||
async _getOrderIdsForCurrentPage(limit, offset) {
|
||||
return await this.rpc({
|
||||
model: 'sale.order',
|
||||
method: 'search_read',
|
||||
args: [this.searchDomain ? this.searchDomain : [], ['name', 'partner_id', 'amount_total', 'date_order', 'state', 'user_id'], offset, limit],
|
||||
context: this.comp.env.session.user_context,
|
||||
});
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.lastPage) {
|
||||
this.currentPage += 1;
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage -= 1;
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {integer|undefined} id id of the cached order
|
||||
* @returns {Array<models.Order>}
|
||||
*/
|
||||
get(id) {
|
||||
return this.ordersToShow;
|
||||
}
|
||||
setSearchDomain(searchDomain) {
|
||||
this.searchDomain = searchDomain;
|
||||
}
|
||||
setComponent(comp) {
|
||||
this.comp = comp;
|
||||
return this;
|
||||
}
|
||||
setNPerPage(val) {
|
||||
this.nPerPage = val;
|
||||
}
|
||||
setPage(page) {
|
||||
this.currentPage = page;
|
||||
}
|
||||
|
||||
async rpc() {
|
||||
Gui.setSyncStatus('connecting');
|
||||
const result = await this.comp.rpc(...arguments);
|
||||
Gui.setSyncStatus('connected');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return new SaleOrderFetcher();
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
odoo.define('pos_sale.SaleOrderList', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useState } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* @props {models.Order} [initHighlightedOrder] initially highligted order
|
||||
* @props {Array<models.Order>} orders
|
||||
*/
|
||||
class SaleOrderList extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click-order', this._onClickOrder);
|
||||
this.state = useState({ highlightedOrder: this.props.initHighlightedOrder || null });
|
||||
}
|
||||
get highlightedOrder() {
|
||||
return this.state.highlightedOrder;
|
||||
}
|
||||
_onClickOrder({ detail: order }) {
|
||||
this.state.highlightedOrder = order;
|
||||
}
|
||||
}
|
||||
SaleOrderList.template = 'SaleOrderList';
|
||||
|
||||
Registries.Component.add(SaleOrderList);
|
||||
|
||||
return SaleOrderList;
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
odoo.define('pos_sale.SaleOrderManagementControlPanel', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useContext } = owl.hooks;
|
||||
const { useAutofocus, useListener } = require('web.custom_hooks');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const SaleOrderFetcher = require('pos_sale.SaleOrderFetcher');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
|
||||
// NOTE: These are constants so that they are only instantiated once
|
||||
// and they can be used efficiently by the OrderManagementControlPanel.
|
||||
const VALID_SEARCH_TAGS = new Set(['date', 'customer', 'client', 'name', 'order']);
|
||||
const FIELD_MAP = {
|
||||
date: 'date_order',
|
||||
customer: 'partner_id.display_name',
|
||||
client: 'partner_id.display_name',
|
||||
name: 'name',
|
||||
order: 'name',
|
||||
};
|
||||
const SEARCH_FIELDS = ['name', 'partner_id.display_name', 'date_order'];
|
||||
|
||||
/**
|
||||
* @emits close-screen
|
||||
* @emits prev-page
|
||||
* @emits next-page
|
||||
* @emits search
|
||||
*/
|
||||
class SaleOrderManagementControlPanel extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// We are using context because we want the `searchString` to be alive
|
||||
// even if this component is destroyed (unmounted).
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
useListener('clear-search', this._onClearSearch);
|
||||
useAutofocus({ selector: 'input' });
|
||||
|
||||
let currentClient = this.env.pos.get_client();
|
||||
if (currentClient) {
|
||||
this.orderManagementContext.searchString = currentClient.name;
|
||||
let domain = this._computeDomain();
|
||||
SaleOrderFetcher.setSearchDomain(domain);
|
||||
}
|
||||
}
|
||||
onInputKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.trigger('search', this._computeDomain());
|
||||
}
|
||||
}
|
||||
get showPageControls() {
|
||||
return SaleOrderFetcher.lastPage > 1;
|
||||
}
|
||||
get pageNumber() {
|
||||
const currentPage = SaleOrderFetcher.currentPage;
|
||||
const lastPage = SaleOrderFetcher.lastPage;
|
||||
return isNaN(lastPage) ? '' : `(${currentPage}/${lastPage})`;
|
||||
}
|
||||
get validSearchTags() {
|
||||
return VALID_SEARCH_TAGS;
|
||||
}
|
||||
get fieldMap() {
|
||||
return FIELD_MAP;
|
||||
}
|
||||
get searchFields() {
|
||||
return SEARCH_FIELDS;
|
||||
}
|
||||
/**
|
||||
* E.g. 1
|
||||
* ```
|
||||
* searchString = 'Customer 1'
|
||||
* result = [
|
||||
* '|',
|
||||
* '|',
|
||||
* ['pos_reference', 'ilike', '%Customer 1%'],
|
||||
* ['partner_id.display_name', 'ilike', '%Customer 1%'],
|
||||
* ['date_order', 'ilike', '%Customer 1%']
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* E.g. 2
|
||||
* ```
|
||||
* searchString = 'date: 2020-05'
|
||||
* result = [
|
||||
* ['date_order', 'ilike', '%2020-05%']
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* E.g. 3
|
||||
* ```
|
||||
* searchString = 'customer: Steward, date: 2020-05-01'
|
||||
* result = [
|
||||
* ['partner_id.display_name', 'ilike', '%Steward%'],
|
||||
* ['date_order', 'ilike', '%2020-05-01%']
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
_computeDomain() {
|
||||
let domain = [['state', '!=', 'cancel'],['invoice_status', '!=', 'invoiced']];
|
||||
const input = this.orderManagementContext.searchString.trim();
|
||||
if (!input) return domain;
|
||||
|
||||
const searchConditions = this.orderManagementContext.searchString.split(/[,&]\s*/);
|
||||
if (searchConditions.length === 1) {
|
||||
let cond = searchConditions[0].split(/:\s*/);
|
||||
if (cond.length === 1) {
|
||||
domain = domain.concat(Array(this.searchFields.length - 1).fill('|'));
|
||||
domain = domain.concat(this.searchFields.map((field) => [field, 'ilike', `%${cond[0]}%`]));
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
for (let cond of searchConditions) {
|
||||
let [tag, value] = cond.split(/:\s*/);
|
||||
if (!this.validSearchTags.has(tag)) continue;
|
||||
domain.push([this.fieldMap[tag], 'ilike', `%${value}%`]);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
_onClearSearch() {
|
||||
this.orderManagementContext.searchString = '';
|
||||
this.onInputKeydown({ key: 'Enter' });
|
||||
}
|
||||
}
|
||||
SaleOrderManagementControlPanel.template = 'SaleOrderManagementControlPanel';
|
||||
|
||||
Registries.Component.add(SaleOrderManagementControlPanel);
|
||||
|
||||
return SaleOrderManagementControlPanel;
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
odoo.define('pos_sale.SaleOrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { sprintf } = require('web.utils');
|
||||
const { parse } = require('web.field_utils');
|
||||
const { useContext } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const SaleOrderFetcher = require('pos_sale.SaleOrderFetcher');
|
||||
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
const models = require('point_of_sale.models');
|
||||
|
||||
class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('close-screen', this.close);
|
||||
useListener('click-sale-order', this._onClickSaleOrder);
|
||||
useListener('next-page', this._onNextPage);
|
||||
useListener('prev-page', this._onPrevPage);
|
||||
useListener('search', this._onSearch);
|
||||
|
||||
SaleOrderFetcher.setComponent(this);
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
}
|
||||
mounted() {
|
||||
SaleOrderFetcher.on('update', this, this.render);
|
||||
this.env.pos.get('orders').on('add remove', this.render, this);
|
||||
|
||||
// calculate how many can fit in the screen.
|
||||
// It is based on the height of the header element.
|
||||
// So the result is only accurate if each row is just single line.
|
||||
const flexContainer = this.el.querySelector('.flex-container');
|
||||
const cpEl = this.el.querySelector('.control-panel');
|
||||
const headerEl = this.el.querySelector('.order-row.header');
|
||||
const val = Math.trunc(
|
||||
(flexContainer.offsetHeight - cpEl.offsetHeight - headerEl.offsetHeight) /
|
||||
headerEl.offsetHeight
|
||||
);
|
||||
SaleOrderFetcher.setNPerPage(val);
|
||||
|
||||
// Fetch the order after mounting so that order management screen
|
||||
// is shown while fetching.
|
||||
setTimeout(() => SaleOrderFetcher.fetch(), 0);
|
||||
}
|
||||
willUnmount() {
|
||||
SaleOrderFetcher.off('update', this);
|
||||
this.env.pos.get('orders').off('add remove', null, this);
|
||||
}
|
||||
get selectedClient() {
|
||||
const order = this.orderManagementContext.selectedOrder;
|
||||
return order ? order.get_client() : null;
|
||||
}
|
||||
get orders() {
|
||||
return SaleOrderFetcher.get();
|
||||
}
|
||||
async _setNumpadMode(event) {
|
||||
const { mode } = event.detail;
|
||||
this.numpadMode = mode;
|
||||
NumberBuffer.reset();
|
||||
}
|
||||
_onNextPage() {
|
||||
SaleOrderFetcher.nextPage();
|
||||
}
|
||||
_onPrevPage() {
|
||||
SaleOrderFetcher.prevPage();
|
||||
}
|
||||
_onSearch({ detail: domain }) {
|
||||
SaleOrderFetcher.setSearchDomain(domain);
|
||||
SaleOrderFetcher.setPage(1);
|
||||
SaleOrderFetcher.fetch();
|
||||
}
|
||||
async _onClickSaleOrder(event) {
|
||||
const clickedOrder = event.detail;
|
||||
const { confirmed, payload: selectedOption } = await this.showPopup('SelectionPopup',
|
||||
{
|
||||
title: this.env._t('What do you want to do?'),
|
||||
list: [{id:"0", label: "Apply a down payment", item: false}, {id:"1", label: "Settle the order", item: true}],
|
||||
});
|
||||
|
||||
if(confirmed){
|
||||
let currentPOSOrder = this.env.pos.get_order();
|
||||
let sale_order = await this._getSaleOrder(clickedOrder.id);
|
||||
try {
|
||||
await this.env.pos.load_new_partners();
|
||||
}
|
||||
catch (error){
|
||||
}
|
||||
currentPOSOrder.set_client(this.env.pos.db.get_partner_by_id(sale_order.partner_id[0]));
|
||||
let orderFiscalPos = sale_order.fiscal_position_id ? this.env.pos.fiscal_positions.find(
|
||||
(position) => position.id === sale_order.fiscal_position_id[0]
|
||||
)
|
||||
: false;
|
||||
if (orderFiscalPos){
|
||||
currentPOSOrder.fiscal_position = orderFiscalPos;
|
||||
}
|
||||
let orderPricelist = sale_order.pricelist_id ? this.env.pos.pricelists.find(
|
||||
(pricelist) => pricelist.id === sale_order.pricelist_id[0]
|
||||
)
|
||||
: false;
|
||||
if (orderPricelist){
|
||||
currentPOSOrder.set_pricelist(orderPricelist);
|
||||
}
|
||||
|
||||
if (selectedOption){
|
||||
// settle the order
|
||||
let lines = sale_order.order_line;
|
||||
let product_to_add_in_pos = lines.filter(line => !this.env.pos.db.get_product_by_id(line.product_id[0])).map(line => line.product_id[0]);
|
||||
if (product_to_add_in_pos.length){
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('Products not available in POS'),
|
||||
body:
|
||||
this.env._t(
|
||||
'Some of the products in your Sale Order are not available in POS, do you want to import them?'
|
||||
),
|
||||
confirmText: this.env._t('Yes'),
|
||||
cancelText: this.env._t('No'),
|
||||
});
|
||||
if (confirmed){
|
||||
await this.env.pos._addProducts(product_to_add_in_pos);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This variable will have 3 values, `undefined | false | true`.
|
||||
* Initially, it is `undefined`. When looping thru each sale.order.line,
|
||||
* when a line comes with lots (`.lot_names`), we use these lot names
|
||||
* as the pack lot of the generated pos.order.line. We ask the user
|
||||
* if he wants to use the lots that come with the sale.order.lines to
|
||||
* be used on the corresponding pos.order.line only once. So, once the
|
||||
* `useLoadedLots` becomes true, it will be true for the succeeding lines,
|
||||
* and vice versa.
|
||||
*/
|
||||
let useLoadedLots;
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
if (!this.env.pos.db.get_product_by_id(line.product_id[0])){
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_line = new models.Orderline({}, {
|
||||
pos: this.env.pos,
|
||||
order: this.env.pos.get_order(),
|
||||
product: this.env.pos.db.get_product_by_id(line.product_id[0]),
|
||||
price: line.price_unit,
|
||||
price_manually_set: true,
|
||||
sale_order_origin_id: clickedOrder,
|
||||
sale_order_line_id: line,
|
||||
customer_note: line.customer_note,
|
||||
});
|
||||
|
||||
if (
|
||||
new_line.get_product().tracking !== 'none' &&
|
||||
(this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots) &&
|
||||
line.lot_names.length > 0
|
||||
) {
|
||||
// Ask once when `useLoadedLots` is undefined, then reuse it's value on the succeeding lines.
|
||||
const { confirmed } =
|
||||
useLoadedLots === undefined
|
||||
? await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('SN/Lots Loading'),
|
||||
body: this.env._t(
|
||||
'Do you want to load the SN/Lots linked to the Sales Order?'
|
||||
),
|
||||
confirmText: this.env._t('Yes'),
|
||||
cancelText: this.env._t('No'),
|
||||
})
|
||||
: { confirmed: useLoadedLots };
|
||||
useLoadedLots = confirmed;
|
||||
if (useLoadedLots) {
|
||||
new_line.setPackLotLines({
|
||||
modifiedPackLotLines: [],
|
||||
newPackLotLines: (line.lot_names || []).map((name) => ({ lot_name: name })),
|
||||
});
|
||||
}
|
||||
}
|
||||
new_line.setQuantityFromSOL(line);
|
||||
new_line.set_unit_price(line.price_unit);
|
||||
new_line.set_discount(line.discount);
|
||||
this.env.pos.get_order().add_orderline(new_line);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// apply a downpayment
|
||||
let lines = sale_order.order_line;
|
||||
let tab = [];
|
||||
|
||||
for (let i=0; i<lines.length; i++) {
|
||||
tab[i] = {
|
||||
'product_name': lines[i].product_id[1],
|
||||
'product_uom_qty': lines[i].product_uom_qty,
|
||||
'price_unit': lines[i].price_unit,
|
||||
'total': lines[i].price_total,
|
||||
};
|
||||
}
|
||||
|
||||
let down_payment = sale_order.amount_total;
|
||||
const { confirmed, payload } = await this.showPopup('NumberPopup', {
|
||||
title: sprintf(this.env._t("Percentage of %s"), this.env.pos.format_currency(sale_order.amount_total)),
|
||||
startingValue: 0,
|
||||
});
|
||||
if (confirmed){
|
||||
down_payment = sale_order.amount_total * parse.float(payload) / 100;
|
||||
}
|
||||
|
||||
|
||||
let new_line = new models.Orderline({}, {
|
||||
pos: this.env.pos,
|
||||
order: this.env.pos.get_order(),
|
||||
product: this.env.pos.db.get_product_by_id(this.env.pos.config.down_payment_product_id[0]),
|
||||
price: down_payment,
|
||||
price_manually_set: true,
|
||||
sale_order_origin_id: clickedOrder,
|
||||
down_payment_details: tab,
|
||||
});
|
||||
new_line.set_unit_price(down_payment);
|
||||
this.env.pos.get_order().add_orderline(new_line);
|
||||
}
|
||||
|
||||
currentPOSOrder.trigger('change');
|
||||
this.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async _getSaleOrder(id) {
|
||||
let sale_order = await this.rpc({
|
||||
model: 'sale.order',
|
||||
method: 'read',
|
||||
args: [[id],['order_line', 'partner_id', 'pricelist_id', 'fiscal_position_id', 'amount_total', 'amount_untaxed']],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
|
||||
let sale_lines = await this._getSOLines(sale_order[0].order_line);
|
||||
sale_order[0].order_line = sale_lines;
|
||||
|
||||
return sale_order[0];
|
||||
}
|
||||
|
||||
async _getSOLines(ids) {
|
||||
let so_lines = await this.rpc({
|
||||
model: 'sale.order.line',
|
||||
method: 'read_converted',
|
||||
args: [ids],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
return so_lines;
|
||||
}
|
||||
|
||||
}
|
||||
SaleOrderManagementScreen.template = 'SaleOrderManagementScreen';
|
||||
SaleOrderManagementScreen.hideOrderSelector = true;
|
||||
|
||||
Registries.Component.add(SaleOrderManagementScreen);
|
||||
|
||||
return SaleOrderManagementScreen;
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
odoo.define('pos_sale.SaleOrderRow', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* @props {models.Order} order
|
||||
* @props columns
|
||||
* @emits click-order
|
||||
*/
|
||||
class SaleOrderRow extends PosComponent {
|
||||
get order() {
|
||||
return this.props.order;
|
||||
}
|
||||
get highlighted() {
|
||||
const highlightedOrder = this.props.highlightedOrder;
|
||||
return !highlightedOrder ? false : highlightedOrder.backendId === this.props.order.backendId;
|
||||
}
|
||||
|
||||
// Column getters //
|
||||
|
||||
get name() {
|
||||
return this.order.name;
|
||||
}
|
||||
get date() {
|
||||
return moment(this.order.date_order).format('YYYY-MM-DD hh:mm A');
|
||||
}
|
||||
get customer() {
|
||||
const customer = this.order.partner_id;
|
||||
return customer ? customer[1] : null;
|
||||
}
|
||||
get total() {
|
||||
return this.env.pos.format_currency(this.order.amount_total);
|
||||
}
|
||||
get state() {
|
||||
let state_mapping = {
|
||||
'draft': this.env._t('Quotation'),
|
||||
'sent': this.env._t('Quotation Sent'),
|
||||
'sale': this.env._t('Sales Order'),
|
||||
'done': this.env._t('Locked'),
|
||||
'cancel': this.env._t('Cancelled'),
|
||||
};
|
||||
|
||||
return state_mapping[this.order.state];
|
||||
}
|
||||
get salesman() {
|
||||
const salesman = this.order.user_id;
|
||||
return salesman ? salesman[1] : null;
|
||||
}
|
||||
}
|
||||
SaleOrderRow.template = 'SaleOrderRow';
|
||||
|
||||
Registries.Component.add(SaleOrderRow);
|
||||
|
||||
return SaleOrderRow;
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
odoo.define('pos_sale.ProductScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosSaleProductScreen = (ProductScreen) =>
|
||||
class extends ProductScreen {
|
||||
async _newOrderlineSelected() {
|
||||
await super._newOrderlineSelected();
|
||||
this._setNumpadModeToPrice();
|
||||
}
|
||||
mounted() {
|
||||
super.mounted();
|
||||
this._setNumpadModeToPrice();
|
||||
}
|
||||
_setNumpadModeToPrice() {
|
||||
if (this.currentOrder) {
|
||||
const selectedOrderline = this.currentOrder.get_selected_orderline();
|
||||
if (selectedOrderline && selectedOrderline.product.id === selectedOrderline.pos.config.down_payment_product_id[0]) {
|
||||
this.state.numpadMode = 'price';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(ProductScreen, PosSaleProductScreen);
|
||||
|
||||
return ProductScreen;
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
odoo.define('pos_sale.SetSaleOrderButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
|
||||
class SetSaleOrderButton extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
mounted() {
|
||||
this.env.pos.get('orders').on('add remove change', () => this.render(), this);
|
||||
this.env.pos.on('change:selectedOrder', () => this.render(), this);
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.pos.get('orders').off('add remove change', null, this);
|
||||
this.env.pos.off('change:selectedOrder', null, this);
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
async onClick() {
|
||||
try {
|
||||
// ping the server, if no error, show the screen
|
||||
await this.rpc({
|
||||
model: 'sale.order',
|
||||
method: 'browse',
|
||||
args: [[]],
|
||||
kwargs: { context: this.env.session.user_context },
|
||||
});
|
||||
this.showScreen('SaleOrderManagementScreen');
|
||||
} catch (error) {
|
||||
if (isRpcError(error) && error.message.code < 0) {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t('Cannot access order management screen if offline.'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SetSaleOrderButton.template = 'SetSaleOrderButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: SetSaleOrderButton,
|
||||
condition: function() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(SetSaleOrderButton);
|
||||
|
||||
return SetSaleOrderButton;
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
odoo.define('pos_sale.models', function (require) {
|
||||
"use strict";
|
||||
|
||||
var models = require('point_of_sale.models');
|
||||
|
||||
|
||||
|
||||
var existing_models = models.PosModel.prototype.models;
|
||||
var product_index = _.findIndex(existing_models, function (model) {
|
||||
return model.model === "product.product";
|
||||
});
|
||||
var product_model = existing_models[product_index];
|
||||
|
||||
models.load_models([{
|
||||
model: product_model.model,
|
||||
fields: product_model.fields,
|
||||
order: product_model.order,
|
||||
domain: function(self) {return [['id', '=', self.config.down_payment_product_id[0]]];},
|
||||
context: product_model.context,
|
||||
loaded: product_model.loaded,
|
||||
}]);
|
||||
|
||||
models.load_fields("product.product", ["invoice_policy", "type"]);
|
||||
|
||||
var super_order_line_model = models.Orderline.prototype;
|
||||
models.Orderline = models.Orderline.extend({
|
||||
initialize: function (attributes, options) {
|
||||
super_order_line_model.initialize.apply(this, arguments);
|
||||
// It is possible that this orderline is initialized using `init_from_JSON`,
|
||||
// meaning, it is loaded from localStorage or from export_for_ui. This means
|
||||
// that some fields has already been assigned. Therefore, we only set the options
|
||||
// when the original value is falsy.
|
||||
this.sale_order_origin_id = this.sale_order_origin_id || options.sale_order_origin_id;
|
||||
this.sale_order_line_id = this.sale_order_line_id || options.sale_order_line_id;
|
||||
this.down_payment_details = this.down_payment_details || options.down_payment_details;
|
||||
this.customerNote = this.customerNote || options.customer_note;
|
||||
},
|
||||
init_from_JSON: function (json) {
|
||||
super_order_line_model.init_from_JSON.apply(this, arguments);
|
||||
this.sale_order_origin_id = json.sale_order_origin_id;
|
||||
this.sale_order_line_id = json.sale_order_line_id;
|
||||
this.down_payment_details = json.down_payment_details && JSON.parse(json.down_payment_details);
|
||||
},
|
||||
export_as_JSON: function () {
|
||||
const json = super_order_line_model.export_as_JSON.apply(this, arguments);
|
||||
json.sale_order_origin_id = this.sale_order_origin_id;
|
||||
json.sale_order_line_id = this.sale_order_line_id;
|
||||
json.down_payment_details = this.down_payment_details && JSON.stringify(this.down_payment_details);
|
||||
return json;
|
||||
},
|
||||
get_sale_order: function(){
|
||||
if(this.sale_order_origin_id) {
|
||||
let value = {
|
||||
'name': this.sale_order_origin_id.name,
|
||||
'details': this.down_payment_details || false
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
export_for_printing: function() {
|
||||
var json = super_order_line_model.export_for_printing.apply(this,arguments);
|
||||
json.down_payment_details = this.down_payment_details;
|
||||
if (this.sale_order_origin_id) {
|
||||
json.so_reference = this.sale_order_origin_id.name;
|
||||
}
|
||||
return json;
|
||||
},
|
||||
/**
|
||||
* Set quantity based on the give sale order line.
|
||||
* @param {'sale.order.line'} saleOrderLine
|
||||
*/
|
||||
setQuantityFromSOL: function(saleOrderLine) {
|
||||
if (this.product.type === 'service') {
|
||||
this.set_quantity(saleOrderLine.qty_to_invoice);
|
||||
} else {
|
||||
this.set_quantity(saleOrderLine.product_uom_qty - Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<div t-name="MobileSaleOrderManagementScreen" class="screen-full-width" owl="1">
|
||||
<div t-if="mobileState.showDetails" class="leftpane">
|
||||
<OrderDetails order="orderManagementContext.selectedOrder" />
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
|
||||
<t t-component="cb.component" t-key="cb.name" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="subpads">
|
||||
<ActionpadWidget client="selectedClient" />
|
||||
<NumpadWidget />
|
||||
</div>
|
||||
</div>
|
||||
<div class="back-to-list" t-on-click="mobileState.showDetails = false">
|
||||
<span>Back to list</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="" class="rightpane">
|
||||
<div class="flex-container">
|
||||
<OrderManagementControlPanel />
|
||||
<OrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SaleOrderList" owl="1">
|
||||
<div class="orders">
|
||||
<div class="order-row header">
|
||||
<div class="header name">Order</div>
|
||||
<div class="header date">Date</div>
|
||||
<div class="header customer">Customer</div>
|
||||
<div class="header salesman">Salesperson</div>
|
||||
<div class="header total">Total</div>
|
||||
<div class="header state">State</div>
|
||||
</div>
|
||||
<div class="order-list">
|
||||
<t t-foreach="props.orders" t-as="order">
|
||||
<SaleOrderRow order="order" highlightedOrder="highlightedOrder" />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SaleOrderManagementControlPanel" owl="1">
|
||||
<div class="control-panel">
|
||||
<div class="item button back" t-on-click="trigger('close-screen')">
|
||||
<i class="fa fa-angle-double-left"></i>
|
||||
<span> Back</span>
|
||||
</div>
|
||||
<div class="item search-box">
|
||||
<span class="icon">
|
||||
<i class="fa fa-search" />
|
||||
</span>
|
||||
<input type="text" t-model="orderManagementContext.searchString" t-on-keydown="onInputKeydown" placeholder="E.g. customer: Steward, date: 2020-05-09" />
|
||||
<span class="clear" t-on-click="trigger('clear-search')">
|
||||
<i class="fa fa-remove" />
|
||||
</span>
|
||||
</div>
|
||||
<div t-if="showPageControls" class="item">
|
||||
<div class="page-controls">
|
||||
<div class="previous" t-on-click="trigger('prev-page')">
|
||||
<i class="fa fa-fw fa-caret-left" role="img" aria-label="Previous Order List" title="Previous Order List"></i>
|
||||
</div>
|
||||
<div class="next" t-on-click="trigger('next-page')">
|
||||
<i class="fa fa-fw fa-caret-right" role="img" aria-label="Next Order List" title="Next Order List"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<span><t t-esc="pageNumber" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="" class="item"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SaleOrderManagementScreen" owl="1">
|
||||
<div class="order-management-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
|
||||
<div t-if="!env.isMobile" class="screen-full-width">
|
||||
<div class="rightpane">
|
||||
<div class="flex-container">
|
||||
<SaleOrderManagementControlPanel />
|
||||
<SaleOrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MobileOrderManagementScreen t-else="" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SaleOrderRow" owl="1">
|
||||
<div class="order-row"
|
||||
t-att-class="{ highlight: highlighted }"
|
||||
t-on-click="trigger('click-sale-order', props.order)">
|
||||
<div class="item name"><t t-esc="name" /></div>
|
||||
<div class="item date"><t t-esc="date" /></div>
|
||||
<div class="item customer"><t t-esc="customer" /></div>
|
||||
<div class="item salesman"><t t-esc="salesman" /></div>
|
||||
<div class="item total"><t t-esc="total" /></div>
|
||||
<div class="item state"><t t-esc="state" /></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||
<t t-if="props.line.get_sale_order()">
|
||||
<li class="info orderline-sale-order">
|
||||
<i class="fa fa-basket" role="img" aria-label="SO" title="SO"/>
|
||||
<t t-esc="props.line.get_sale_order().name" />
|
||||
</li>
|
||||
<table t-if="props.line.get_sale_order().details" class="sale-order-info">
|
||||
<t t-foreach='props.line.get_sale_order().details' t-as='line' t-key='line.id'>
|
||||
<tr>
|
||||
<td><t t-esc="line['product_uom_qty']"/>x</td>
|
||||
<td style="max-width: 275px;">
|
||||
<t t-esc="line['product_name']" />
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td><t t-esc="env.pos.format_currency(line['total'])" /> (tax incl.)</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderReceipt" t-inherit="point_of_sale.OrderLinesReceipt" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//t[@t-foreach]" position="inside">
|
||||
<div class="pos-receipt-left-padding" t-if="line.so_reference">From <t t-esc="line.so_reference"/></div>
|
||||
<div class="pos-receipt-left-padding" t-if="line.down_payment_details">
|
||||
<table class="sale-order-info">
|
||||
<tr t-foreach='line.down_payment_details' t-as='line' t-key='line.id'>
|
||||
<td><t t-esc="line['product_uom_qty']" />x</td>
|
||||
<td style="max-width: 200px;">
|
||||
<t t-esc="line['product_name']" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SetSaleOrderButton" owl="1">
|
||||
<div class="control-button o_sale_order_button">
|
||||
<i class="fa fa-link" role="img" aria-label="Set Sale Order"
|
||||
title="Set Sale Order" /> Quotation/Order
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_invoice_document" inherit_id="account.report_invoice_document">
|
||||
<xpath expr="//td[@name='account_invoice_line_name']" position="inside">
|
||||
<t t-if="o.pos_order_ids">
|
||||
<div style="margin-left:15px; font-style: italic">
|
||||
<t t-set="down_payment_product" t-value="o.pos_order_ids[0].config_id.down_payment_product_id"/>
|
||||
<t t-if="line.product_id == down_payment_product">
|
||||
<t t-set="sale_orders" t-value="o.pos_order_ids[0].lines.filtered(lambda l: l.product_id == down_payment_product and l.price_subtotal_incl == line.price_total).mapped('sale_order_origin_id')"/>
|
||||
<t t-if="sale_orders">
|
||||
<t t-set="sale_order" t-value="sale_orders[0]"/>
|
||||
<t t-foreach="sale_order.order_line" t-as="sale_order_line">
|
||||
<t t-if="sale_order_line.product_id != down_payment_product">
|
||||
<div>
|
||||
<span style="margin-right: 5px;"><t t-esc="int(sale_order_line.product_uom_qty)"/>x</span>
|
||||
<span t-esc="sale_order_line.name" />
|
||||
<span style="margin: 0px 5px;">:</span>
|
||||
<t t-esc="sale_order_line.price_total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<div style="font-weight: bold">From <t t-esc="sale_order.name"/> </div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//p[@name='payment_communication']" position="replace">
|
||||
<t t-if="not o.pos_order_ids">
|
||||
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.payment_reference" name="payment_communication">
|
||||
Please use the following communication for your payment : <b><span t-field="o.payment_reference"/></b>
|
||||
</p>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='total']/div/table" position="inside">
|
||||
<t t-if="o.pos_order_ids">
|
||||
<tr>
|
||||
<td>
|
||||
<i class="oe_form_field text-right oe_payment_label">Paid on <t t-esc="o.invoice_date" t-options='{"widget": "date"}'/></i>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="o.pos_order_ids.mapped('payment_ids')" t-as="payment">
|
||||
<tr>
|
||||
<td>
|
||||
Paid with <t t-esc="payment.payment_method_id.name"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<t t-esc="payment.amount" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div id="accounting_section" position="after">
|
||||
<h2>Sales Reporting</h2>
|
||||
<h2>Sales</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
@@ -18,6 +18,19 @@
|
||||
<field name="crm_team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Down Payment Product</span>
|
||||
<div class="text-muted">
|
||||
This product will be applied when down payment is made
|
||||
</div>
|
||||
<field
|
||||
name="down_payment_product_id"
|
||||
domain="[('type', '=', 'service'), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"
|
||||
context="{'default_type': 'service', 'default_taxes_id': False }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_pos_order_form_inherit_pos_sale" model="ir.ui.view">
|
||||
<field name="name">pos.order.form.pos.sale</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_invoice']" position="before">
|
||||
<button type="object"
|
||||
name="action_view_sale_order"
|
||||
class="oe_stat_button"
|
||||
icon="fa-shopping-basket"
|
||||
attrs="{'invisible': [('sale_order_count', '=', 0)]}" groups="sales_team.group_sale_salesman">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="sale_order_count" widget="statinfo" nolabel="1" class="mr4" /> Transfered<br/>
|
||||
from Sale
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_order_form_inherit_pos_sale" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.pos.sale</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_invoice']" position="before">
|
||||
<button type="object"
|
||||
name="action_view_pos_order"
|
||||
class="oe_stat_button"
|
||||
icon="fa-shopping-basket"
|
||||
attrs="{'invisible': [('pos_order_count', '=', 0)]}" groups="point_of_sale.group_pos_user">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="pos_order_count" widget="statinfo" nolabel="1" class="mr4" /> Transfered<br/>
|
||||
to POS
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="message_body" inherit_id="stock.message_body">
|
||||
<xpath expr="//t[contains(@t-if, 'move.state')]" position="inside">
|
||||
<t t-set="pos_orders" t-value="move.sale_line_id.pos_order_line_ids.mapped('order_id')" />
|
||||
<t t-if="len(pos_orders)">
|
||||
<li>
|
||||
Delivered from
|
||||
<t t-foreach="pos_orders" t-as="pos_order">
|
||||
<a href="#" t-att-data-oe-model="pos_order._name" t-att-data-oe-id="pos_order.id">
|
||||
<t t-esc="pos_order.display_name" />
|
||||
</a><span t-if="pos_orders.ids[-1:] != pos_order.ids">, </span>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -326,7 +326,7 @@ class SaleOrderLine(models.Model):
|
||||
# task_global_project: create task in global project
|
||||
for so_line in so_line_task_global_project:
|
||||
if not so_line.task_id:
|
||||
if map_sol_project.get(so_line.id):
|
||||
if map_sol_project.get(so_line.id) and so_line.product_uom_qty > 0:
|
||||
so_line._timesheet_create_task(project=map_sol_project[so_line.id])
|
||||
|
||||
# project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too.
|
||||
|
||||
+8
-8
@@ -1409,7 +1409,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
# convert default values to the right format
|
||||
#
|
||||
# we explicitly avoid using _convert_to_write() for x2many fields,
|
||||
# because the latter leaves values like [(Command.LINK, 2),
|
||||
# because the latter leaves values like [(Command.LINK, 2),
|
||||
# (Command.LINK, 3)], which are not supported by the web client as
|
||||
# default values; stepping through the cache allows to normalize
|
||||
# such a list to [(Command.SET, 0, [2, 3])], which is properly
|
||||
@@ -2316,8 +2316,8 @@ class BaseModel(metaclass=MetaModel):
|
||||
@api.model
|
||||
def _read_group_format_result(self, data, annotated_groupbys, groupby, domain):
|
||||
"""
|
||||
Helper method to format the data contained in the dictionary data by
|
||||
adding the domain corresponding to its values, the groupbys in the
|
||||
Helper method to format the data contained in the dictionary data by
|
||||
adding the domain corresponding to its values, the groupbys in the
|
||||
context and by properly formatting the date/datetime values.
|
||||
|
||||
:param data: a single group
|
||||
@@ -2396,10 +2396,10 @@ class BaseModel(metaclass=MetaModel):
|
||||
The possible aggregation functions are the ones provided by PostgreSQL
|
||||
(https://www.postgresql.org/docs/current/static/functions-aggregate.html)
|
||||
and 'count_distinct', with the expected meaning.
|
||||
:param list groupby: list of groupby descriptions by which the records will be grouped.
|
||||
:param list groupby: list of groupby descriptions by which the records will be grouped.
|
||||
A groupby description is either a field (then it will be grouped by that field)
|
||||
or a string 'field:groupby_function'. Right now, the only functions supported
|
||||
are 'day', 'week', 'month', 'quarter' or 'year', and they only make sense for
|
||||
are 'day', 'week', 'month', 'quarter' or 'year', and they only make sense for
|
||||
date/datetime fields.
|
||||
:param int offset: optional number of records to skip
|
||||
:param int limit: optional max number of records to return
|
||||
@@ -2407,7 +2407,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
overriding the natural sort ordering of the
|
||||
groups, see also :py:meth:`~osv.osv.osv.search`
|
||||
(supported only for many2one fields currently)
|
||||
:param bool lazy: if true, the results are only grouped by the first groupby and the
|
||||
:param bool lazy: if true, the results are only grouped by the first groupby and the
|
||||
remaining groupbys are put in the __context key. If false, all the groupbys are
|
||||
done in one call.
|
||||
:return: list of dictionaries(one dictionary for each record) containing:
|
||||
@@ -4132,7 +4132,7 @@ Fields:
|
||||
# that this limit is well managed by PostgreSQL.
|
||||
# In INSERT queries, we inject integers (small) and larger data (TEXT blocks for
|
||||
# example).
|
||||
#
|
||||
#
|
||||
# The problem then becomes: how to "estimate" the right size of the batch to have
|
||||
# good performance?
|
||||
#
|
||||
@@ -6540,7 +6540,7 @@ Fields:
|
||||
field_generators = self._populate_factories()
|
||||
if not field_generators:
|
||||
return self.browse() # maybe create an automatic generator?
|
||||
|
||||
|
||||
records_batches = []
|
||||
generator = populate.chain_factories(field_generators, self._name)
|
||||
while record_count <= min_size or not complete:
|
||||
|
||||
Reference in New Issue
Block a user