19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -4,4 +4,6 @@
from . import pos_details
from . import pos_payment
from . import pos_close_session_wizard
from . import pos_session_check_product_wizard
from . import pos_daily_sales_reports
from . import pos_confirmation_wizard
from . import pos_make_invoice

View file

@ -5,7 +5,7 @@ from odoo import fields, models
class PosCloseSessionWizard(models.TransientModel):
_name = "pos.close.session.wizard"
_name = 'pos.close.session.wizard'
_description = "Close Session Wizard"
amount_to_balance = fields.Float("Amount to balance")

View file

@ -9,11 +9,11 @@
<group>
<field name="account_readonly" invisible="1" />
<field name="amount_to_balance" readonly="1" />
<field name="account_id" attrs="{'readonly': [('account_readonly', '==', True)]}"/>
<field name="account_id" readonly="account_readonly"/>
</group>
<footer>
<button name="close_session" string="Close Session" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel" class="btn-secondary" />
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary" />
</footer>
</form>
</field>

View file

@ -0,0 +1,31 @@
from odoo import models, fields, api, _
class PosConfirmationWizard(models.TransientModel):
_name = 'pos.confirmation.wizard'
_description = 'Confirmation Wizard'
def get_selected_orders(self):
selected_orders = self.env.context.get('orders')
return self.env['pos.order'].browse(selected_orders)
def _default_message(self):
selected_orders = self.get_selected_orders()
customer_name = selected_orders.partner_id.name
message = _("It seems that the POS order(s) %(order_ref)s do not have a customer.\n\nWould you like to set %(customer_name)s as the customer for the selected POS order(s)?", order_ref= ', '.join(selected_orders.filtered(lambda o: not o.partner_id).mapped('name')), customer_name=customer_name)
return message
message = fields.Text(default=_default_message, readonly=True)
def action_confirm(self):
selected_orders = self.get_selected_orders()
selected_orders.write({'partner_id': selected_orders.partner_id.id})
return {
'name': _('Create Invoice(s)'),
'view_mode': 'form',
'view_id': self.env.ref('point_of_sale.view_pos_make_invoice').id,
'res_model': 'pos.make.invoice',
'target': 'new',
'type': 'ir.actions.act_window',
'context': {'active_ids': selected_orders.ids},
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="point_of_sale.view_confirm_action_wizard" model="ir.ui.view">
<field name="name">pos.confirmation.wizard.form</field>
<field name="model">pos.confirmation.wizard</field>
<field name="arch" type="xml">
<form string="Warning" width="500px">
<group>
<field name="message" nolabel="1" readonly="1"/>
</group>
<footer>
<button name="action_confirm" string="Confirm" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_confirm_action_wizard" model="ir.actions.act_window">
<field name="name">Confirm Action</field>
<field name="res_model">pos.confirmation.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="point_of_sale.view_confirm_action_wizard"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class PosDailySalesReportsWizard(models.TransientModel):
_name = 'pos.daily.sales.reports.wizard'
_description = 'Point of Sale Daily Report'
pos_session_id = fields.Many2one('pos.session', required=True)
def _get_report_data(self):
return {'date_start': False, 'date_stop': False, 'config_ids': self.pos_session_id.config_id.ids, 'session_ids': self.pos_session_id.ids}
def generate_report(self):
return self.env.ref('point_of_sale.sale_details_report').report_action([], data=self._get_report_data())

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_daily_sales_reports_wizard" model="ir.ui.view">
<field name="name">pos.daily.sales.reports.wizard.form</field>
<field name="model">pos.daily.sales.reports.wizard</field>
<field name="arch" type="xml">
<form string="Sales Details">
<group name="pos_session_group">
<field name="pos_session_id" mode="list"/>
</group>
<footer>
<button name="generate_report" string="Print" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_report_pos_daily_sales_reports" model="ir.actions.act_window">
<field name="name">Session Report</field>
<field name="res_model">pos.daily.sales.reports.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -1,31 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from datetime import timedelta
from odoo import api, fields, models
class PosDetails(models.TransientModel):
class PosDetailsWizard(models.TransientModel):
_name = 'pos.details.wizard'
_description = 'Point of Sale Details Report'
def _default_start_date(self):
""" Find the earliest start_date of the latests sessions """
# restrict to configs available to the user
config_ids = self.env['pos.config'].search([]).ids
# exclude configs has not been opened for 2 days
self.env.cr.execute("""
SELECT
max(start_at) as start,
config_id
FROM pos_session
WHERE config_id = ANY(%s)
AND start_at > (NOW() - INTERVAL '2 DAYS')
GROUP BY config_id
""", (config_ids,))
latest_start_dates = [res['start'] for res in self.env.cr.dictfetchall()]
# earliest of the latest sessions
return latest_start_dates and min(latest_start_dates) or fields.Datetime.now()
values = self.env['pos.session']._read_group([
('config_id', '!=', False),
('start_at', '>', self.env.cr.now() - timedelta(days=2))
], groupby=['config_id'], aggregates=['start_at:max'])
mapping = dict(values)
return (mapping and min(mapping.values())) or self.env.cr.now()
start_date = fields.Datetime(required=True, default=_default_start_date)
end_date = fields.Datetime(required=True, default=fields.Datetime.now)

View file

@ -10,11 +10,11 @@
<field name="end_date"/>
</group>
<group>
<field name="pos_config_ids" mode="tree" colspan="2" nolabel="1" />
<field name="pos_config_ids" mode="list" nolabel="1" />
</group>
<footer>
<button name="generate_report" string="Print" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
</field>

View file

@ -0,0 +1,70 @@
from odoo import models, fields, _
from odoo.exceptions import UserError
class PosMakeInvoice(models.TransientModel):
_name = 'pos.make.invoice'
_description = 'Multiple order invoice creation'
consolidated_billing = fields.Boolean(
string="Consolidated Billing", default=True,
help="Create one invoice for all orders related to same customer and same invoicing address"
)
count = fields.Integer(string="Order Count", compute='_compute_order_count')
def _compute_order_count(self):
for wizard in self:
wizard.count = len(self.env.context.get('active_ids', []))
def action_create_invoices(self):
self.ensure_one()
selected_orders = self.env['pos.order'].browse(self.env.context.get('active_ids')).filtered(lambda o: o.invoice_status == 'to_invoice' and o.state != 'draft' and o.state != 'cancel')
if not selected_orders:
raise UserError(_("No valid orders were selected. No new invoices could be generated"))
is_single_order = len(selected_orders) == 1
invalid_refund_orders = selected_orders.filtered(lambda o: o.refunded_order_id.account_move)
if (not is_single_order) and invalid_refund_orders:
# Normally it can't be encountered because when paying a refund order,
# if the original order is invoiced, the refund order is required by the UI to be invoiced.
order_names = "\n".join([f"{o.name} ({o.pos_reference})" for o in invalid_refund_orders])
raise UserError(_("The following refund orders can't be part of a consolidated invoice because they refunded invoiced orders. Each refund order should be handled separately.\n\n%s", order_names))
invoices = self.env['account.move']
if not self.consolidated_billing or len(selected_orders) == 1:
for order in selected_orders:
invoices |= order._generate_pos_order_invoice()
else:
configs = selected_orders.config_id
partners = selected_orders.partner_id
some_order_has_no_partner = any(not o.partner_id for o in selected_orders)
if len(configs) == 1 and len(partners) == 1 and some_order_has_no_partner:
# When all the orders belong to one config and there is only one customer but some orders have no partner,
# we can proceed but we ask the user if we proceed by setting that one customer to all the orders.
return {
'name': _('Warning'),
'view_mode': 'form',
'view_id': self.env.ref('point_of_sale.view_confirm_action_wizard').id,
'res_model': 'pos.confirmation.wizard',
'target': 'new',
'type': 'ir.actions.act_window',
'context': {'orders': selected_orders.ids, 'dialog_size': 'medium'},
}
grouped_orders = []
for config, config_orders in selected_orders.grouped('config_id').items():
for partner, partner_orders in config_orders.grouped('partner_id').items():
if not partner:
raise UserError(_("Kindly ensure that each order contains a customer."))
for user, user_orders in partner_orders.grouped('user_id').items():
for fiscal_position, fiscal_position_orders in user_orders.grouped('fiscal_position_id').items():
grouped_orders.append(((config, partner, user, fiscal_position), fiscal_position_orders))
for _key, orders in grouped_orders:
invoices |= orders._generate_pos_order_invoice()
if invoices:
return selected_orders.action_view_invoice()

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="point_of_sale.view_pos_make_invoice" model="ir.ui.view">
<field name="name">Create Invoice(s)</field>
<field name="model">pos.make.invoice</field>
<field name="arch" type="xml">
<form>
<group>
<field name="count"/>
<field name="consolidated_billing" invisible="count == 1"/>
</group>
<footer>
<button name="action_create_invoices" type="object"
string="Create"
class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -22,8 +22,8 @@ class PosMakePayment(models.TransientModel):
order = self.env['pos.order'].browse(active_id)
amount_total = order.amount_total
# If we refund the entire order, we refund what was paid originally, else we refund the value of the items returned
if float_is_zero(order.refunded_order_ids.amount_total + order.amount_total, precision_rounding=order.currency_id.rounding):
amount_total = -order.refunded_order_ids.amount_paid
if float_is_zero(order.refunded_order_id.amount_total + order.amount_total, precision_rounding=order.currency_id.rounding):
amount_total = -order.refunded_order_id.amount_paid
return amount_total - order.amount_paid
return False
@ -57,18 +57,20 @@ class PosMakePayment(models.TransientModel):
currency = order.currency_id
init_data = self.read()[0]
payment_method = self.env['pos.payment.method'].browse(init_data['payment_method_id'][0])
if not float_is_zero(init_data['amount'], precision_rounding=currency.rounding):
order.add_payment({
'pos_order_id': order.id,
'amount': order._get_rounded_amount(init_data['amount']),
'amount': order._get_rounded_amount(init_data['amount'], payment_method.is_cash_count or not self.config_id.only_round_cash_method),
'name': init_data['payment_name'],
'payment_method_id': init_data['payment_method_id'][0],
})
if order._is_pos_order_paid():
order.action_pos_order_paid()
order._create_order_picking()
order._compute_total_cost_in_real_time()
if order.state == 'draft' and order._is_pos_order_paid():
order._process_saved_order(False)
if order.state in {'paid', 'done'}:
order._send_order()
order.config_id.notify_synchronisation(order.config_id.current_session_id.id, 0)
return {'type': 'ir.actions.act_window_close'}
return self.launch_payment()

View file

@ -13,14 +13,13 @@
</group>
<footer>
<button name="check" string="Make Payment" type="object" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel" class="btn-secondary"/>
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="action_pos_payment" model="ir.actions.act_window">
<field name="name">Payment</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.make.payment</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View file

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.exceptions import AccessDenied
from odoo.tools import convert
class PosSessionCheckProductWizard(models.TransientModel):
_name = 'pos.session.check_product_wizard'
_description = 'Verify if there are any products for the PoS'
def load_demo_products(self):
if not self.env.user.has_group("point_of_sale.group_pos_user"):
raise AccessDenied()
convert.convert_file(self.env.cr, 'point_of_sale', 'data/point_of_sale_onboarding.xml', None, mode='init', kind='data')
return self.open_ui()
def open_ui(self):
config = self.env['pos.config'].browse(self.env.context.get('config_id'))
return config._action_to_open_ui()

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_session_check_product_wizard" model="ir.ui.view">
<field name="name">pos.session.check.product.wizard.form</field>
<field name="model">pos.session.check_product_wizard</field>
<field name="arch" type="xml">
<form string="Check products">
<p>
You can add some from the Products menu, or linked any existing by flagging them as "Available in PoS".
Or you can add demo data for testing purpose. Please mind that this is an irreversible action.
</p>
<footer>
<button name="load_demo_products" string="Add demo data" type="object" class="btn-primary" data-hotkey="q"/>
<button name="open_ui" string="Continue without Demo data" type="object" class="btn-secondary" data-hotkey="z" />
</footer>
</form>
</field>
</record>
</odoo>