mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 08:32:02 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue