Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,26 @@
odoo.define('point_of_sale.MobileSaleOrderManagementScreen', function (require) {
const SaleOrderManagementScreen = require('pos_sale.SaleOrderManagementScreen');
const Registries = require('point_of_sale.Registries');
const { useListener } = require("@web/core/utils/hooks");
const { useState } = owl;
const MobileSaleOrderManagementScreen = (SaleOrderManagementScreen) => {
class MobileSaleOrderManagementScreen extends SaleOrderManagementScreen {
setup() {
super.setup();
useListener('click-order', this._onShowDetails)
this.mobileState = useState({ showDetails: false });
}
_onShowDetails() {
this.mobileState.showDetails = true;
}
}
MobileSaleOrderManagementScreen.template = 'MobileSaleOrderManagementScreen';
return MobileSaleOrderManagementScreen;
};
Registries.Component.addByExtending(MobileSaleOrderManagementScreen, SaleOrderManagementScreen);
return MobileSaleOrderManagementScreen;
});

View file

@ -0,0 +1,137 @@
odoo.define('pos_sale.SaleOrderFetcher', function (require) {
'use strict';
const { Gui } = require('point_of_sale.Gui');
const { isConnectionError } = require('point_of_sale.utils');
const { EventBus } = owl;
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;
}
get orderFields(){
return ['name', 'partner_id', 'amount_total', 'date_order', 'state', 'user_id', 'amount_unpaid']
}
/**
* 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;
// 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 (isConnectionError(error)) {
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) {
let domain = [['currency_id', '=', this.comp.env.pos.currency.id]].concat(this.searchDomain || []);
const saleOrders = await this.rpc({
model: 'sale.order',
method: 'search_read',
args: [domain, this.orderFields, offset, limit],
context: this.comp.env.session.user_context,
});
return saleOrders;
}
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();
});

View file

@ -0,0 +1,32 @@
odoo.define('pos_sale.SaleOrderList', function (require) {
'use strict';
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { useState } = owl;
/**
* @props {models.Order} [initHighlightedOrder] initially highligted order
* @props {Array<models.Order>} orders
*/
class SaleOrderList extends PosComponent {
setup() {
super.setup();
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;
});

View file

@ -0,0 +1,127 @@
odoo.define('pos_sale.SaleOrderManagementControlPanel', function (require) {
'use strict';
const { useAutofocus, useListener } = require("@web/core/utils/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');
const { useState } = owl;
// 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 {
setup() {
super.setup();
this.orderManagementContext = useState(contexts.orderManagement);
useListener('clear-search', this._onClearSearch);
useAutofocus();
let currentPartner = this.env.pos.get_order().get_partner();
if (currentPartner) {
this.orderManagementContext.searchString = currentPartner.name;
}
SaleOrderFetcher.setSearchDomain(this._computeDomain());
}
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;
});

View file

@ -0,0 +1,357 @@
odoo.define('pos_sale.SaleOrderManagementScreen', function (require) {
'use strict';
const { sprintf } = require('web.utils');
const { parse } = require('web.field_utils');
const { _t } = require('@web/core/l10n/translation');
const { useListener } = require("@web/core/utils/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 utils = require('web.utils');
const { Orderline } = require('point_of_sale.models');
const { onMounted, onWillUnmount, useState } = owl;
/**
* ID getter to take into account falsy many2one value.
* @param {[id: number, display_name: string] | false} fieldVal many2one field value
* @returns {number | false}
*/
function getId(fieldVal) {
return fieldVal && fieldVal[0];
}
class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) {
setup() {
super.setup();
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 = useState(contexts.orderManagement);
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
onMounted() {
SaleOrderFetcher.on('update', this, this.render);
// 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('.header-row');
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);
}
onWillUnmount() {
SaleOrderFetcher.off('update', this);
}
get selectedPartner() {
const order = this.orderManagementContext.selectedOrder;
return order ? order.get_partner() : 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();
}
_getSaleOrderOrigin(order) {
for (const line of order.get_orderlines()) {
if (line.sale_order_origin_id) {
return line.sale_order_origin_id
}
}
return false;
}
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: this.env._t("Apply a down payment"), item: false}, {id:"1", label: this.env._t("Settle the order"), item: true}],
});
if(confirmed){
let currentPOSOrder = this.env.pos.get_order();
let sale_order = await this._getSaleOrder(clickedOrder.id);
const currentSaleOrigin = this._getSaleOrderOrigin(currentPOSOrder);
const currentSaleOriginId = currentSaleOrigin && currentSaleOrigin.id;
if (currentSaleOriginId) {
const linkedSO = await this._getSaleOrder(currentSaleOriginId);
if (
getId(linkedSO.partner_id) !== getId(sale_order.partner_id) ||
getId(linkedSO.partner_invoice_id) !== getId(sale_order.partner_invoice_id) ||
getId(linkedSO.partner_shipping_id) !== getId(sale_order.partner_shipping_id)
) {
currentPOSOrder = this.env.pos.add_new_order();
this.showNotification(this.env._t("A new order has been created."));
}
}
let order_partner = this.env.pos.db.get_partner_by_id(sale_order.partner_id[0])
if(order_partner){
currentPOSOrder.set_partner(order_partner);
} else {
try {
await this.env.pos._loadPartners([sale_order.partner_id[0]]);
}
catch (_error){
const title = this.env._t('Customer loading error');
const body = _.str.sprintf(this.env._t('There was a problem in loading the %s customer.'), sale_order.partner_id[1]);
await this.showPopup('ErrorPopup', { title, body });
}
currentPOSOrder.set_partner(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;
}
const line_values = {
pos: this.env.pos,
order: this.env.pos.get_order(),
product: this.env.pos.db.get_product_by_id(line.product_id[0]),
description: line.product_id[1],
price: line.price_unit,
tax_ids: orderFiscalPos ? undefined : line.tax_id,
price_automatically_set: true,
price_manually_set: false,
sale_order_origin_id: clickedOrder,
sale_order_line_id: line,
customer_note: line.customer_note,
};
let new_line = Orderline.create({}, line_values);
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);
const product = this.env.pos.db.get_product_by_id(line.product_id[0]);
const product_unit = product.get_unit();
if (product_unit && !product.get_unit().is_pos_groupable) {
//loop for value of quantity
let remaining_quantity = new_line.quantity;
while (!utils.float_is_zero(remaining_quantity, 6)) {
let splitted_line = Orderline.create({}, line_values);
splitted_line.set_quantity(Math.min(remaining_quantity, 1.0), true);
splitted_line.set_discount(line.discount);
remaining_quantity -= splitted_line.quantity;
this.env.pos.get_order().add_orderline(splitted_line);
}
}
else {
this.env.pos.get_order().add_orderline(new_line);
}
}
}
else {
// apply a downpayment
if (this.env.pos.config.down_payment_product_id){
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_product = this.env.pos.db.get_product_by_id(this.env.pos.config.down_payment_product_id[0]);
if (!down_payment_product) {
await this.env.pos._addProducts([this.env.pos.config.down_payment_product_id[0]]);
down_payment_product = this.env.pos.db.get_product_by_id(this.env.pos.config.down_payment_product_id[0]);
}
let down_payment_tax = this.env.pos.taxes_by_id[down_payment_product.taxes_id] || false ;
let down_payment;
if (down_payment_tax) {
down_payment = down_payment_tax.price_include ? sale_order.amount_total : sale_order.amount_untaxed;
}
else{
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 = down_payment * parse.float(payload) / 100;
}
if (down_payment > sale_order.amount_unpaid) {
const errorBody = sprintf(
this.env._t("You have tried to charge a down payment of %s but only %s remains to be paid, %s will be applied to the purchase order line."),
this.env.pos.format_currency(down_payment),
this.env.pos.format_currency(sale_order.amount_unpaid),
sale_order.amount_unpaid > 0 ? this.env.pos.format_currency(sale_order.amount_unpaid) : this.env.pos.format_currency(0),
);
await this.showPopup('ErrorPopup', { title: _t('Error amount too high'), body: errorBody });
down_payment = sale_order.amount_unpaid > 0 ? sale_order.amount_unpaid : 0;
}
let new_line = Orderline.create({}, {
pos: this.env.pos,
order: this.env.pos.get_order(),
product: down_payment_product,
price: down_payment,
price_automatically_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);
}
else {
const title = this.env._t('No down payment product');
const body = this.env._t(
"It seems that you didn't configure a down payment product in your point of sale.\
You can go to your point of sale configuration to choose one."
);
await this.showPopup('ErrorPopup', { title, body });
}
}
this.close();
}
}
async _getSaleOrder(id) {
const 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', 'amount_unpaid', 'partner_shipping_id', 'partner_invoice_id']],
context: this.env.session.user_context,
});
const 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;
});

View file

@ -0,0 +1,71 @@
odoo.define('pos_sale.SaleOrderRow', function (require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const utils = require('web.utils');
const { deserializeDateTime } = require("@web/core/l10n/dates");
/**
* @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 deserializeDateTime(this.order.date_order).toFormat("yyyy-MM-dd HH:mm a");
}
get partner() {
const partner = this.order.partner_id;
return partner ? partner[1] : null;
}
get total() {
return this.env.pos.format_currency(this.order.amount_total);
}
/**
* Returns true if the order has unpaid amount, but the unpaid amount
* should not be the same as the total amount.
* @returns {boolean}
*/
get showAmountUnpaid() {
const isFullAmountUnpaid = utils.float_is_zero(Math.abs(this.order.amount_total - this.order.amount_unpaid), this.env.pos.currency.decimal_places);
return !isFullAmountUnpaid && !utils.float_is_zero(this.order.amount_unpaid, this.env.pos.currency.decimal_places);
}
get amountUnpaidRepr() {
return this.env.pos.format_currency(this.order.amount_unpaid);
}
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;
});

View file

@ -0,0 +1,59 @@
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/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
const { isConnectionError } = require('point_of_sale.utils');
const { Gui } = require('point_of_sale.Gui');
class SetSaleOrderButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
async onClick() {
try {
// ping the server, if no error, show the screen
// Use rpc from services which resolves even when this
// component is destroyed (removed together with the popup).
await this.env.services.rpc({
model: 'sale.order',
method: 'browse',
args: [[]],
kwargs: { context: this.env.session.user_context },
});
// LegacyComponent doesn't work the same way as before.
// We need to use Gui here to show the screen. This will work
// because ui methods in Gui is bound to the root component.
const screen = this.env.isMobile ? 'MobileSaleOrderManagementScreen' : 'SaleOrderManagementScreen';
Gui.showScreen(screen);
} catch (error) {
if (isConnectionError(error)) {
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;
});

View file

@ -0,0 +1,83 @@
odoo.define('pos_sale.models', function (require) {
"use strict";
var { Order, Orderline } = require('point_of_sale.models');
const Registries = require('point_of_sale.Registries');
const PosSaleOrder = (Order) => class PosSaleOrder extends Order {
//@override
select_orderline(orderline) {
super.select_orderline(...arguments);
if (orderline && orderline.product.id === this.pos.config.down_payment_product_id[0]) {
this.pos.numpadMode = 'price';
}
}
//@override
_get_ignored_product_ids_total_discount() {
const productIds = super._get_ignored_product_ids_total_discount(...arguments);
productIds.push(this.pos.config.down_payment_product_id[0]);
return productIds;
}
}
Registries.Model.extend(Order, PosSaleOrder);
const PosSaleOrderline = (Orderline) => class PosSaleOrderline extends Orderline {
constructor(obj, options) {
super(...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(json) {
super.init_from_JSON(...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() {
const json = super.export_as_JSON(...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(){
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() {
var json = super.export_for_printing(...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(saleOrderLine) {
if (this.product.type === 'service' && !['sent', 'draft'].includes(this.sale_order_origin_id.state)) {
this.set_quantity(saleOrderLine.qty_to_invoice);
} else {
this.set_quantity(saleOrderLine.product_uom_qty - Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced));
}
}
}
Registries.Model.extend(Orderline, PosSaleOrderline);
});