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

@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import account_move_line
from . import pos_config
from . import pos_order
from . import product_template
from . import crm_team
from . import pos_session
from . import sale_order
from . import stock_picking
from . import res_config_settings
from . import res_partner

View file

@ -0,0 +1,35 @@
from odoo import models, _
class AccountMove(models.Model):
_inherit = 'account.move'
def reflect_cancelled_sol(self, isCancelled):
if self.env.user.has_group('point_of_sale.group_pos_user'):
for invoice in self:
for pos_order_line in invoice.pos_order_ids.mapped('lines'):
if pos_order_line.sale_order_line_id:
if isCancelled and "(Cancelled)" not in pos_order_line.sale_order_line_id.name:
name = _("%(old_name)s (Cancelled)", old_name=pos_order_line.sale_order_line_id.name)
pos_order_line.sale_order_line_id.name = name
elif not isCancelled and "(Cancelled)" in pos_order_line.sale_order_line_id.name:
pos_order_line.sale_order_line_id.name = pos_order_line.sale_order_line_id.name.replace(" (Cancelled)", "")
def button_cancel(self):
res = super().button_cancel()
self.reflect_cancelled_sol(True)
return res
def action_post(self):
res = super().action_post()
self.reflect_cancelled_sol(False)
return res
def _is_downpayment(self):
# EXTENDS sale
self.ensure_one()
if self.line_ids.sale_line_ids:
return super()._is_downpayment()
base_lines, _ = self._get_rounded_base_and_tax_lines()
return base_lines and all('down_payment' in (line['computation_key'] or '').split(',') for line in base_lines)

View file

@ -0,0 +1,46 @@
from odoo import models
from odoo.tools import float_compare
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _get_downpayment_lines(self):
# EXTENDS sale
downpayment_lines = self.env["account.move.line"]
if not self.env['pos.order.line'].has_access('read'):
return super()._get_downpayment_lines()
for record in self:
rounding = record.currency_id.rounding
if related_sol := record.sale_line_ids:
# if order is not settled through POS
# We're assuming that if an order is settled through POS it will have sale_line_ids empty.
# We're also assuming that the downpayment line will have the same price_subtotal & tax_ids as the record.
pos_downpayment_moves = related_sol.filtered("is_downpayment").pos_order_line_ids.order_id.account_move
downpayment_lines |= pos_downpayment_moves.invoice_line_ids.filtered(
lambda r: float_compare(r.price_subtotal, -record.price_subtotal, precision_rounding=rounding) == 0
and r.tax_ids == record.tax_ids,
)
elif related_posl := record.move_id.pos_order_ids.lines:
# if order is settled through POS
# We get the downpayment lines through:
# final invoice -> final POS order -> origin sale order -> downpayment pos order -> downpayment invoice
sale_orders = related_posl.sale_order_origin_id
candidate_moves = sale_orders.pos_order_line_ids.order_id.account_move.filtered(lambda r: r._is_downpayment())
applicable_lines = candidate_moves.invoice_line_ids.filtered(
lambda line: float_compare(line.price_subtotal, -record.price_subtotal, precision_rounding=rounding) == 0
and line.tax_ids == record.tax_ids,
)
if len(applicable_lines) > 1:
# In the case there are multiple downpayment lines with the same tax & total we'll
# pair them up as per their ids and get the downpayment line paired with the record.
move_lines = record.move_id.invoice_line_ids
lines_dict = dict(zip(move_lines.sorted('id'), applicable_lines.sorted('id')))
downpayment_lines |= lines_dict.get(record)
else:
downpayment_lines |= applicable_lines
return downpayment_lines | super()._get_downpayment_lines()

View file

@ -1,31 +1,7 @@
# -*- 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, ValidationError
from datetime import datetime
import pytz
from odoo import fields, models
class CrmTeam(models.Model):
_inherit = 'crm.team'
pos_config_ids = fields.One2many('pos.config', 'crm_team_id', string="Point of Sales")
pos_sessions_open_count = fields.Integer(string='Open POS Sessions', compute='_compute_pos_sessions_open_count')
pos_order_amount_total = fields.Float(string="Session Sale Amount", compute='_compute_pos_order_amount_total')
def _compute_pos_sessions_open_count(self):
for team in self:
team.pos_sessions_open_count = self.env['pos.session'].search_count([('config_id.crm_team_id', '=', team.id), ('state', '=', 'opened')])
def _compute_pos_order_amount_total(self):
data = self.env['report.pos.order']._read_group([
('session_id.state', '=', 'opened'),
('config_id.crm_team_id', 'in', self.ids),
], ['price_total:sum', 'config_id'], ['config_id'])
rg_results = dict((d['config_id'][0], d['price_total']) for d in data)
for team in self:
team.pos_order_amount_total = sum([
rg_results.get(config.id, 0.0)
for config in team.pos_config_ids
])

View file

@ -1,15 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import fields, models, api
class PosConfig(models.Model):
_inherit = 'pos.config'
crm_team_id = fields.Many2one(
'crm.team', string="Sales Team", ondelete="set null",
'crm.team', string="Sales Team", ondelete="set null", index='btree_not_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.")
def _get_special_products(self):
res = super()._get_special_products()
return res | self.env['pos.config'].search([]).mapped('down_payment_product_id')
@api.model
def _ensure_downpayment_product(self):
pos_config = self.env.ref('point_of_sale.pos_config_main', raise_if_not_found=False)
downpayment_product = self.env.ref('pos_sale.default_downpayment_product', raise_if_not_found=False)
if pos_config and downpayment_product:
pos_config.write({'down_payment_product_id': downpayment_product.id})
@api.model
def load_onboarding_furniture_scenario(self, with_demo_data=True):
res = super().load_onboarding_furniture_scenario(with_demo_data)
self._ensure_downpayment_product()
return res

View file

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from functools import lru_cache
from odoo import api, fields, models, _
from odoo.tools import float_compare, float_is_zero
from odoo.tools import float_compare, float_is_zero, format_date
class PosOrder(models.Model):
@ -20,24 +19,14 @@ class PosOrder(models.Model):
@api.model
def _complete_values_from_session(self, session, values):
values = super(PosOrder, self)._complete_values_from_session(session, values)
values.setdefault('crm_team_id', session.config_id.crm_team_id.id)
values['crm_team_id'] = values['crm_team_id'] if values.get('crm_team_id') else session.config_id.crm_team_id.id
return values
@api.depends('pricelist_id.currency_id', 'date_order', 'company_id')
@api.depends('date_order', 'company_id')
def _compute_currency_rate(self):
@lru_cache
def get_rate(from_currency, to_currency, company, date):
return self.env['res.currency']._get_conversion_rate(
from_currency=from_currency,
to_currency=to_currency,
company=company,
date=date,
)
for order in self:
date_order = order.date_order or fields.Datetime.now()
# date_order is a datetime, but the rates are looked up on a date basis,
# therefor converting the date_order to a date helps with sharing entries in the lru_cache
order.currency_rate = get_rate(order.company_id.currency_id, order.pricelist_id.currency_id, order.company_id, date_order.date())
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, date_order.date())
def _prepare_invoice_vals(self):
invoice_vals = super(PosOrder, self)._prepare_invoice_vals()
@ -49,65 +38,103 @@ class PosOrder(models.Model):
else:
addr = self.partner_id.address_get(['delivery'])
invoice_vals['partner_shipping_id'] = addr['delivery']
if sale_orders[0].payment_term_id:
if sale_orders[0].payment_term_id and not sale_orders[0].payment_term_id.early_discount:
invoice_vals['invoice_payment_term_id'] = sale_orders[0].payment_term_id.id
else:
invoice_vals['invoice_payment_term_id'] = False
if sale_orders[0].partner_invoice_id != sale_orders[0].partner_id:
invoice_vals['partner_id'] = sale_orders[0].partner_invoice_id.id
return invoice_vals
def _create_order_picking(self):
for line in self.lines.filtered(lambda l: l.product_id == self.config_id.down_payment_product_id and l.qty != 0 and (l.sale_order_origin_id or l.refunded_orderline_id.sale_order_origin_id)):
sale_lines = line.sale_order_origin_id.order_line or line.refunded_orderline_id.sale_order_origin_id.order_line
sale_order_origin = line.sale_order_origin_id or line.refunded_orderline_id.sale_order_origin_id
sale_line = self.env['sale.order.line'].create({
'order_id': sale_order_origin.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,
})
line.sale_order_line_id = sale_line
def action_pos_order_paid(self):
res = super().action_pos_order_paid()
if any(p.payment_method_id._is_online_payment() for p in self.payment_ids):
sale_orders = self.lines.mapped('sale_order_origin_id')
for so in sale_orders.filtered(lambda s: s.state in ('draft', 'sent')):
so.action_confirm()
return res
so_lines = self.lines.mapped('sale_order_line_id')
@api.model
def sync_from_ui(self, orders):
data = super().sync_from_ui(orders)
if len(orders) == 0:
return data
# 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_recordset(['qty_delivered'])
# track the waiting pickings
waiting_picking_ids = set()
for so_line in so_lines:
so_line_stock_move_ids = so_line.move_ids.group_id.stock_move_ids
for stock_move in so_line.move_ids:
picking = stock_move.picking_id
if picking.state not in ['waiting', 'confirmed', 'assigned']:
AccountTax = self.env['account.tax']
pos_orders = self.browse([o['id'] for o in data["pos.order"]])
for pos_order in pos_orders:
# TODO: the way to retrieve the sale order in not consistent... is it a bad code or intended?
used_pos_lines = pos_order.lines.sale_order_origin_id.order_line.pos_order_line_ids
downpayment_pos_order_lines = pos_order.lines.filtered(lambda line: (
line not in used_pos_lines
and line.product_id == pos_order.config_id.down_payment_product_id
))
so_x_pos_order_lines = downpayment_pos_order_lines\
.grouped(lambda l: l.sale_order_origin_id or l.refunded_orderline_id.sale_order_origin_id)
sale_orders = self.env['sale.order']
for sale_order, pos_order_lines in so_x_pos_order_lines.items():
if not sale_order:
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.compute_uom_qty(new_qty, stock_move, False)
# If the product is delivered with more than one step, we need to update the quantity of the other steps
for move in so_line_stock_move_ids.filtered(lambda m: m.state in ['waiting', 'confirmed', 'assigned'] and m.product_id == stock_move.product_id):
move.product_uom_qty = stock_move.product_uom_qty
waiting_picking_ids.add(move.picking_id.id)
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)
sale_orders += sale_order
down_payment_base_lines = pos_order_lines._prepare_tax_base_line_values()
AccountTax._add_tax_details_in_base_lines(down_payment_base_lines, sale_order.company_id)
AccountTax._round_base_lines_tax_details(down_payment_base_lines, sale_order.company_id)
# 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_ids):
picking.action_cancel()
return super()._create_order_picking()
sale_order_sudo = sale_order.sudo()
sale_order_sudo._create_down_payment_section_line_if_needed()
sale_order_sudo._create_down_payment_lines_from_base_lines(down_payment_base_lines)
# Confirm the unconfirmed sale orders that are linked to the sale order lines.
so_lines = pos_order.lines.mapped('sale_order_line_id')
sale_orders |= so_lines.mapped('order_id')
if pos_order.state != 'draft':
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_recordset(['qty_delivered'])
# track the waiting pickings
waiting_picking_ids = set()
for so_line in so_lines:
so_line_stock_move_ids = so_line.move_ids.reference_ids.move_ids
for stock_move in so_line.move_ids:
picking = stock_move.picking_id
if not picking.state in ['waiting', 'confirmed', 'assigned']:
continue
def get_expected_qty_to_ship_later():
pos_pickings = so_line.pos_order_line_ids.order_id.picking_ids
if pos_pickings and all(pos_picking.state in ['confirmed', 'assigned'] for pos_picking in pos_pickings):
return sum((so_line._convert_qty(so_line, pos_line.qty, 'p2s') for pos_line in
so_line.pos_order_line_ids if so_line.product_id.type != 'service'), 0)
return 0
qty_delivered = max(so_line.qty_delivered, get_expected_qty_to_ship_later())
new_qty = so_line.product_uom_qty - qty_delivered
if stock_move.product_uom.compare(new_qty, 0) <= 0:
new_qty = 0
stock_move.product_uom_qty = so_line.compute_uom_qty(new_qty, stock_move, False)
# If the product is delivered with more than one step, we need to update the quantity of the other steps
for move in so_line_stock_move_ids.filtered(lambda m: m.state in ['waiting', 'confirmed', 'assigned'] and m.product_id == stock_move.product_id):
move.product_uom_qty = stock_move.product_uom_qty
waiting_picking_ids.add(move.picking_id.id)
waiting_picking_ids.add(picking.id)
def is_product_uom_qty_zero(move):
return move.product_uom.is_zero(move.product_uom_qty)
# 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_ids):
picking.action_cancel()
else:
# We make sure that the original picking still has the correct quantity reserved
picking.action_assign()
return data
def action_view_sale_order(self):
self.ensure_one()
@ -116,7 +143,7 @@ class PosOrder(models.Model):
'type': 'ir.actions.act_window',
'name': _('Linked Sale Orders'),
'res_model': 'sale.order',
'view_mode': 'tree,form',
'view_mode': 'list,form',
'domain': [('id', 'in', linked_orders.ids)],
}
@ -142,27 +169,72 @@ class PosOrder(models.Model):
}
return order_line
def _get_invoice_lines_values(self, line_values, pos_line, move_type):
inv_line_vals = super()._get_invoice_lines_values(line_values, pos_line, move_type)
if pos_line.sale_order_origin_id:
origin_line = pos_line.sale_order_line_id
inv_line_vals["name"] = origin_line.name
origin_line._set_analytic_distribution(inv_line_vals)
return inv_line_vals
def write(self, vals):
if 'crm_team_id' in vals:
vals['crm_team_id'] = vals['crm_team_id'] if vals.get('crm_team_id') else self.session_id.crm_team_id.id
return super().write(vals)
def _force_create_picking_real_time(self):
result = super()._force_create_picking_real_time()
return result or any(self.lines.mapped('sale_order_origin_id'))
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")
sale_order_origin_id = fields.Many2one('sale.order', string="Linked Sale Order", index='btree_not_null')
sale_order_line_id = fields.Many2one('sale.order.line', string="Source Sale Order Line", index='btree_not_null')
down_payment_details = fields.Text(string="Down Payment Details")
qty_delivered = fields.Float(
string="Delivery Quantity",
compute="_compute_qty_delivered",
store=True, readonly=False, copy=False)
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
@api.depends('order_id.state', 'order_id.picking_ids', 'order_id.picking_ids.state', 'order_id.picking_ids.move_ids.quantity')
def _compute_qty_delivered(self):
product_qty_left_to_assign = {}
for order_line in self:
if order_line.order_id.state in ['paid', 'done']:
outgoing_pickings = order_line.order_id.picking_ids.filtered(
lambda pick: pick.state == 'done' and pick.picking_type_code == 'outgoing'
)
def _order_line_fields(self, line, session_id=None):
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):
#We need to make sure the order line has not been deleted while the order was being handled in the PoS
order_line = self.env['sale.order.line'].search([('id', '=', vals['sale_order_line_id']['id'])], limit=1)
vals['sale_order_line_id'] = order_line.id if order_line else False
return result
if outgoing_pickings and order_line.order_id.shipping_date:
moves = outgoing_pickings.move_ids.filtered(
lambda m: m.state == 'done' and m.product_id == order_line.product_id
)
qty_left = product_qty_left_to_assign.get(order_line.product_id.id, False)
if (qty_left):
order_line.qty_delivered = min(order_line.qty, qty_left)
product_qty_left_to_assign[order_line.product_id.id] -= order_line.qty_delivered
else:
order_line.qty_delivered = min(order_line.qty, sum(moves.mapped('quantity')))
product_qty_left_to_assign[order_line.product_id.id] = sum(moves.mapped('quantity')) - order_line.qty_delivered
elif outgoing_pickings:
# If the order is not delivered later, and in a "paid", "done" or "invoiced" state, it fully delivered
order_line.qty_delivered = order_line.qty
else:
order_line.qty_delivered = 0
@api.model
def _load_pos_data_fields(self, config):
params = super()._load_pos_data_fields(config)
params += ['sale_order_origin_id', 'sale_order_line_id', 'down_payment_details']
return params
def _launch_stock_rule_from_pos_order_lines(self):
orders = self.mapped('order_id')
for order in orders:
self.env['stock.move'].browse(order.lines.sale_order_line_id.move_ids._rollup_move_origs()).filtered(lambda ml: ml.state not in ['cancel', 'done'])._action_cancel()
return super()._launch_stock_rule_from_pos_order_lines()

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.osv.expression import OR
from odoo import fields, models, api
class PosSession(models.Model):
@ -10,8 +9,8 @@ class PosSession(models.Model):
crm_team_id = fields.Many2one('crm.team', related='config_id.crm_team_id', string="Sales Team", readonly=True)
def _loader_params_product_product(self):
result = super()._loader_params_product_product()
result['search_params']['domain'] = OR([result['search_params']['domain'], [('id', '=', self.config_id.down_payment_product_id.id)]])
result['search_params']['fields'].extend(['invoice_policy', 'type'])
return result
@api.model
def _load_pos_data_models(self, config):
data = super()._load_pos_data_models(config)
data += ['sale.order', 'sale.order.line']
return data

View file

@ -0,0 +1,11 @@
from odoo import models, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.model
def _load_pos_data_fields(self, config):
params = super()._load_pos_data_fields(config)
params += ['invoice_policy', 'type', 'sale_line_warn_msg']
return params

View file

@ -0,0 +1,9 @@
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _load_pos_data_fields(self, config):
return super()._load_pos_data_fields(config) + ['sale_warn_msg']

View file

@ -1,15 +1,53 @@
# -*- 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, _, Command
from odoo.tools import format_date
class SaleOrder(models.Model):
_inherit = 'sale.order'
_name = 'sale.order'
_inherit = ['sale.order', 'pos.load.mixin']
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")
amount_unpaid = fields.Monetary(string='Unpaid Amount', compute='_compute_amount_unpaid', store=True, help="The amount due from the sale order.")
amount_unpaid = fields.Monetary(
string="Amount To Pay In POS",
help="Amount left to pay in POS to avoid double payment or double invoicing.",
compute='_compute_amount_unpaid',
store=True,
)
@api.model
def _load_pos_data_domain(self, data, config):
return [['pos_order_line_ids.order_id.state', '=', 'draft']]
@api.model
def _load_pos_data_fields(self, config):
return ['name', 'state', 'user_id', 'order_line', 'partner_id', 'pricelist_id', 'fiscal_position_id', 'amount_total', 'amount_untaxed', 'amount_unpaid',
'picking_ids', 'partner_shipping_id', 'partner_invoice_id', 'date_order', 'write_date', 'amount_paid']
def load_sale_order_from_pos(self, config_id):
product_ids = self.order_line.product_id.ids
product_tmpls = self.env['product.template'].load_product_from_pos(
config_id,
[('product_variant_ids.id', 'in', product_ids)]
)
sale_order_fields = self._load_pos_data_fields(config_id)
sale_order_read = self.read(sale_order_fields, load=False)
sale_order_line_fields = self.order_line._load_pos_data_fields(config_id)
sale_order_line_read = self.order_line.read(sale_order_line_fields, load=False)
sale_order_fp_fields = self.env['account.fiscal.position']._load_pos_data_fields(config_id)
sale_order_fp_read = self.fiscal_position_id.read(sale_order_fp_fields, load=False)
partner_fields = self.env['res.partner']._load_pos_data_fields(config_id)
return {
'sale.order': sale_order_read,
'sale.order.line': sale_order_line_read,
'account.fiscal.position': sale_order_fp_read,
'res.partner': self.partner_id.read(partner_fields, load=False),
**product_tmpls,
}
def _count_pos_order(self):
for order in self:
@ -23,48 +61,131 @@ class SaleOrder(models.Model):
'type': 'ir.actions.act_window',
'name': _('Linked POS Orders'),
'res_model': 'pos.order',
'view_mode': 'tree,form',
'view_mode': 'list,form',
'domain': [('id', 'in', linked_orders.ids)],
}
@api.depends('order_line', 'amount_total', 'order_line.invoice_lines.parent_state', 'order_line.invoice_lines.price_total', 'order_line.pos_order_line_ids')
@api.depends('transaction_ids.state', 'transaction_ids.amount', 'order_line', 'amount_total', 'order_line.invoice_lines.parent_state', 'order_line.invoice_lines.price_total', 'order_line.pos_order_line_ids')
def _compute_amount_unpaid(self):
for sale_order in self:
total_invoice_paid = sum(sale_order.order_line.filtered(lambda l: not l.display_type).mapped('invoice_lines').filtered(lambda l: l.parent_state != 'cancel').mapped('price_total'))
total_pos_paid = sum(sale_order.order_line.filtered(lambda l: not l.display_type).mapped('pos_order_line_ids.price_subtotal_incl'))
sale_order.amount_unpaid = sale_order.amount_total - (total_invoice_paid + total_pos_paid)
invoices = sale_order.order_line.invoice_lines.move_id.filtered(lambda invoice: invoice.state in ('draft', 'posted'))
total_invoices_paid = sum(invoices.mapped('amount_total'))
pos_orders = sale_order.order_line.pos_order_line_ids.order_id
total_pos_orders_paid = sum(pos_orders.mapped('amount_total'))
sale_order.amount_unpaid = max(sale_order.amount_total - total_invoices_paid - total_pos_orders_paid - sale_order.amount_paid, 0.0)
@api.depends('order_line.pos_order_line_ids')
def _compute_amount_to_invoice(self):
super()._compute_amount_to_invoice()
for order in self:
# We need to account for all amount paid in POS with and without invoice
order_amount = sum(order.sudo().pos_order_line_ids.mapped('price_subtotal_incl'))
order.amount_to_invoice -= order_amount
@api.depends('order_line.pos_order_line_ids')
def _compute_amount_invoiced(self):
super()._compute_amount_invoiced()
for order in self:
if order.invoice_status == 'invoiced':
continue
# We need to account for the downpayment paid in POS with and without invoice
order_amount = sum(order.sudo().pos_order_line_ids.filtered(lambda pol: pol.order_id.state in ['paid', 'done', 'invoiced'] and pol.sale_order_line_id.is_downpayment).mapped('price_subtotal_incl'))
order.amount_invoiced += order_amount
def _prepare_down_payment_line_values_from_base_line(self, base_line):
# EXTENDS 'sale'
so_line_values = super()._prepare_down_payment_line_values_from_base_line(base_line)
if (
base_line
and base_line['record']
and isinstance(base_line['record'], models.Model)
and base_line['record']._name == 'pos.order.line'
):
pos_order_line = base_line['record']
so_line_values['name'] = _(
"Down payment (ref: %(order_reference)s on \n %(date)s)",
order_reference=pos_order_line.name,
date=format_date(pos_order_line.env, pos_order_line.order_id.date_order),
)
so_line_values['pos_order_line_ids'] = [Command.set(pos_order_line.ids)]
return so_line_values
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
_name = 'sale.order.line'
_inherit = ['sale.order.line', 'pos.load.mixin']
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', 'pos_order_line_ids.order_id.picking_ids', 'pos_order_line_ids.order_id.picking_ids.state')
@api.model
def _load_pos_data_domain(self, data, config):
return [('order_id', 'in', [order['id'] for order in data['sale.order']])]
@api.model
def _load_pos_data_fields(self, config):
return ['discount', 'display_name', 'price_total', 'price_unit', 'product_id', 'product_uom_qty', 'qty_delivered',
'qty_invoiced', 'qty_to_invoice', 'display_type', 'name', 'tax_ids', 'is_downpayment', 'extra_tax_data',
'write_date', 'product_custom_attribute_value_ids'
]
@api.depends('pos_order_line_ids.qty', 'pos_order_line_ids.order_id.picking_ids', 'pos_order_line_ids.order_id.picking_ids.state', 'pos_order_line_ids.refund_orderline_ids.order_id.picking_ids.state')
def _compute_qty_delivered(self):
super()._compute_qty_delivered()
for sale_line in self:
if all(picking.state == 'done' for picking in sale_line.pos_order_line_ids.order_id.picking_ids):
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 _prepare_qty_delivered(self):
delivered_qties = super()._prepare_qty_delivered()
def _get_pos_delivered_qty(sale_line, pos_lines):
if all(picking.state == "done" for picking in pos_lines.order_id.picking_ids):
# Sum converted quantities from POS to sale order UoM
return sum(self._convert_qty(sale_line, pos_line.qty, "p2s") for pos_line in pos_lines)
return 0
def line_filter(line):
return line.order_id.state not in ["cancel", "draft"]
for sale_line in self.filtered(lambda line: line.product_id.type != "service"):
pos_line_ids = sale_line.sudo().pos_order_line_ids
pos_qty = _get_pos_delivered_qty(sale_line, pos_line_ids.filtered(line_filter))
if pos_qty != 0:
delivered_qties[sale_line] += pos_qty
refund_qty = _get_pos_delivered_qty(sale_line, pos_line_ids.refund_orderline_ids.filtered(line_filter))
if refund_qty != 0:
delivered_qties[sale_line] += refund_qty
return delivered_qties
@api.depends('pos_order_line_ids.qty', 'pos_order_line_ids.order_id.state')
def _compute_qty_invoiced(self):
super()._compute_qty_invoiced()
def _prepare_qty_invoiced(self):
invoiced_qties = super()._prepare_qty_invoiced()
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)
pos_lines = sale_line.sudo().pos_order_line_ids.filtered(lambda order_line: order_line.order_id.state not in ['cancel', 'draft'])
invoiced_qties[sale_line] += sum((
self._convert_qty(sale_line, pos_line.qty, 'p2s') for pos_line in pos_lines
), 0)
return invoiced_qties
def _get_sale_order_fields(self):
return ["product_id", "display_name", "price_unit", "product_uom_qty", "tax_id", "qty_delivered", "qty_invoiced", "discount", "qty_to_invoice", "price_total"]
return ["product_id", "display_name", "price_unit", "product_uom_qty", "tax_ids", "qty_delivered", "qty_invoiced", "discount", "qty_to_invoice", "price_total", "is_downpayment"]
def read_converted(self):
field_names = self._get_sale_order_fields()
results = []
for sale_line in self:
if sale_line.product_type:
if sale_line.product_type or (sale_line.is_downpayment and sale_line.price_unit != 0):
product_uom = sale_line.product_id.uom_id
sale_line_uom = sale_line.product_uom
item = sale_line.read(field_names)[0]
sale_line_uom = sale_line.product_uom_id
item = sale_line.read(field_names, load=False)[0]
if sale_line.product_id.tracking != 'none':
item['lot_names'] = sale_line.move_ids.move_line_ids.lot_id.mapped('name')
move_lines = sale_line.move_ids.move_line_ids.filtered(lambda ml: ml.product_id.id == sale_line.product_id.id)
item['lot_names'] = move_lines.lot_id.mapped('name')
lot_qty_by_name = {}
for line in move_lines:
lot_qty_by_name[line.lot_id.name] = lot_qty_by_name.get(line.lot_id.name, 0.0) + line.quantity
item['lot_qty_by_name'] = lot_qty_by_name
if product_uom == sale_line_uom:
results.append(item)
continue
@ -93,7 +214,7 @@ class SaleOrderLine(models.Model):
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
sale_line_uom = sale_line.product_uom_id
if direction == 's2p':
return sale_line_uom._compute_quantity(qty, product_uom, False)
elif direction == 'p2s':
@ -108,9 +229,19 @@ class SaleOrderLine(models.Model):
def _compute_untaxed_amount_invoiced(self):
super()._compute_untaxed_amount_invoiced()
for line in self:
line.untaxed_amount_invoiced += sum(line.pos_order_line_ids.mapped('price_subtotal'))
line.untaxed_amount_invoiced += sum(line.sudo().pos_order_line_ids.mapped('price_subtotal'))
def _get_downpayment_line_price_unit(self, invoices):
return super()._get_downpayment_line_price_unit(invoices) + sum(
pol.price_unit for pol in self.pos_order_line_ids
pol.price_unit for pol in self.sudo().pos_order_line_ids
)
@api.depends('product_id', 'pos_order_line_ids')
def _compute_name(self):
for sol in self:
if sol.sudo().pos_order_line_ids:
downpayment_sols = sol.pos_order_line_ids.mapped('refunded_orderline_id.sale_order_line_id')
for downpayment_sol in downpayment_sols:
downpayment_sol.name = _("%(line_description)s (Cancelled)", line_description=downpayment_sol.name)
else:
super()._compute_name()

View file

@ -10,7 +10,7 @@ class StockPicking(models.Model):
def _create_move_from_pos_order_lines(self, lines):
lines_to_unreserve = self.env['pos.order.line']
for line in lines:
if line.order_id.to_ship:
if line.order_id.shipping_date:
continue
if any(wh != line.order_id.config_id.warehouse_id for wh in line.sale_order_line_id.move_ids.location_id.warehouse_id):
continue