1131 lines
56 KiB
Python
1131 lines
56 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
|
import logging
|
|
from datetime import timedelta
|
|
from functools import partial
|
|
|
|
import psycopg2
|
|
import pytz
|
|
|
|
from flectra import api, fields, models, tools, _
|
|
from flectra.tools import float_is_zero
|
|
from flectra.exceptions import UserError
|
|
from flectra.http import request
|
|
from flectra.addons import decimal_precision as dp
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PosOrder(models.Model):
|
|
_name = "pos.order"
|
|
_description = "Point of Sale Orders"
|
|
_order = "id desc"
|
|
|
|
@api.model
|
|
def _amount_line_tax(self, line, fiscal_position_id):
|
|
taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id)
|
|
if fiscal_position_id:
|
|
taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id)
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
taxes = taxes.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes']
|
|
return sum(tax.get('amount', 0.0) for tax in taxes)
|
|
|
|
@api.model
|
|
def _order_fields(self, ui_order):
|
|
process_line = partial(self.env['pos.order.line']._order_line_fields, session_id=ui_order['pos_session_id'])
|
|
return {
|
|
'name': ui_order['name'],
|
|
'user_id': ui_order['user_id'] or False,
|
|
'session_id': ui_order['pos_session_id'],
|
|
'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False,
|
|
'pos_reference': ui_order['name'],
|
|
'partner_id': ui_order['partner_id'] or False,
|
|
'date_order': ui_order['creation_date'],
|
|
'fiscal_position_id': ui_order['fiscal_position_id'],
|
|
'pricelist_id': ui_order['pricelist_id'],
|
|
}
|
|
|
|
def _payment_fields(self, ui_paymentline):
|
|
payment_date = ui_paymentline['name']
|
|
payment_date = fields.Date.context_today(self, fields.Datetime.from_string(payment_date))
|
|
return {
|
|
'amount': ui_paymentline['amount'] or 0.0,
|
|
'payment_date': payment_date,
|
|
'statement_id': ui_paymentline['statement_id'],
|
|
'payment_name': ui_paymentline.get('note', False),
|
|
'journal': ui_paymentline['journal_id'],
|
|
}
|
|
|
|
# This deals with orders that belong to a closed session. In order
|
|
# to recover from this situation we create a new rescue session,
|
|
# making it obvious that something went wrong.
|
|
# A new, separate, rescue session is preferred for every such recovery,
|
|
# to avoid adding unrelated orders to live sessions.
|
|
def _get_valid_session(self, order):
|
|
PosSession = self.env['pos.session']
|
|
closed_session = PosSession.browse(order['pos_session_id'])
|
|
|
|
_logger.warning('session %s (ID: %s) was closed but received order %s (total: %s) belonging to it',
|
|
closed_session.name,
|
|
closed_session.id,
|
|
order['name'],
|
|
order['amount_total'])
|
|
rescue_session = PosSession.search([
|
|
('state', 'not in', ('closed', 'closing_control')),
|
|
('rescue', '=', True),
|
|
('config_id', '=', closed_session.config_id.id),
|
|
], limit=1)
|
|
if rescue_session:
|
|
_logger.warning('reusing recovery session %s for saving order %s', rescue_session.name, order['name'])
|
|
return rescue_session
|
|
|
|
_logger.warning('attempting to create recovery session for saving order %s', order['name'])
|
|
new_session = PosSession.create({
|
|
'config_id': closed_session.config_id.id,
|
|
'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name},
|
|
'rescue': True, # avoid conflict with live sessions
|
|
})
|
|
# bypass opening_control (necessary when using cash control)
|
|
new_session.action_pos_session_open()
|
|
|
|
return new_session
|
|
|
|
def _match_payment_to_invoice(self, order):
|
|
account_precision = self.env['decimal.precision'].precision_get('Account')
|
|
|
|
# ignore orders with an amount_paid of 0 because those are returns through the POS
|
|
if not float_is_zero(order['amount_return'], account_precision) and not float_is_zero(order['amount_paid'], account_precision):
|
|
cur_amount_paid = 0
|
|
payments_to_keep = []
|
|
for payment in order.get('statement_ids'):
|
|
if cur_amount_paid + payment[2]['amount'] > order['amount_total']:
|
|
payment[2]['amount'] = order['amount_total'] - cur_amount_paid
|
|
payments_to_keep.append(payment)
|
|
break
|
|
cur_amount_paid += payment[2]['amount']
|
|
payments_to_keep.append(payment)
|
|
order['statement_ids'] = payments_to_keep
|
|
order['amount_return'] = 0
|
|
|
|
@api.model
|
|
def _process_order(self, pos_order):
|
|
prec_acc = self.env['decimal.precision'].precision_get('Account')
|
|
pos_session = self.env['pos.session'].browse(pos_order['pos_session_id'])
|
|
if pos_session.state == 'closing_control' or pos_session.state == 'closed':
|
|
pos_order['pos_session_id'] = self._get_valid_session(pos_order).id
|
|
order = self.create(self._order_fields(pos_order))
|
|
journal_ids = set()
|
|
for payments in pos_order['statement_ids']:
|
|
if not float_is_zero(payments[2]['amount'], precision_digits=prec_acc):
|
|
order.add_payment(self._payment_fields(payments[2]))
|
|
journal_ids.add(payments[2]['journal_id'])
|
|
|
|
if pos_session.sequence_number <= pos_order['sequence_number']:
|
|
pos_session.write({'sequence_number': pos_order['sequence_number'] + 1})
|
|
pos_session.refresh()
|
|
|
|
if not float_is_zero(pos_order['amount_return'], prec_acc):
|
|
cash_journal_id = pos_session.cash_journal_id.id
|
|
if not cash_journal_id:
|
|
# Select for change one of the cash journals used in this
|
|
# payment
|
|
cash_journal = self.env['account.journal'].search([
|
|
('type', '=', 'cash'),
|
|
('id', 'in', list(journal_ids)),
|
|
], limit=1)
|
|
if not cash_journal:
|
|
# If none, select for change one of the cash journals of the POS
|
|
# This is used for example when a customer pays by credit card
|
|
# an amount higher than total amount of the order and gets cash back
|
|
cash_journal = [statement.journal_id for statement in pos_session.statement_ids if statement.journal_id.type == 'cash']
|
|
if not cash_journal:
|
|
raise UserError(_("No cash statement found for this session. Unable to record returned cash."))
|
|
cash_journal_id = cash_journal[0].id
|
|
order.add_payment({
|
|
'amount': -pos_order['amount_return'],
|
|
'payment_date': fields.Datetime.now(),
|
|
'payment_name': _('return'),
|
|
'journal': cash_journal_id,
|
|
})
|
|
return order
|
|
|
|
def _prepare_analytic_account(self, line):
|
|
'''This method is designed to be inherited in a custom module'''
|
|
return False
|
|
|
|
def _create_account_move(self, dt, ref, journal_id, company_id):
|
|
date_tz_user = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(dt))
|
|
date_tz_user = fields.Date.to_string(date_tz_user)
|
|
return self.env['account.move'].sudo().create({'ref': ref, 'journal_id': journal_id, 'date': date_tz_user})
|
|
|
|
def _prepare_invoice(self):
|
|
"""
|
|
Prepare the dict of values to create the new invoice for a pos order.
|
|
"""
|
|
invoice_type = 'out_invoice' if self.amount_total >= 0 else 'out_refund'
|
|
return {
|
|
'name': self.name,
|
|
'origin': self.name,
|
|
'account_id': self.partner_id.property_account_receivable_id.id,
|
|
'journal_id': self.session_id.config_id.invoice_journal_id.id,
|
|
'company_id': self.company_id.id,
|
|
'type': invoice_type,
|
|
'reference': self.name,
|
|
'partner_id': self.partner_id.id,
|
|
'comment': self.note or '',
|
|
# considering partner's sale pricelist's currency
|
|
'currency_id': self.pricelist_id.currency_id.id,
|
|
'user_id': self.user_id.id,
|
|
}
|
|
|
|
@api.model
|
|
def _get_account_move_line_group_data_type_key(self, data_type, values):
|
|
"""
|
|
Return a tuple which will be used as a key for grouping account
|
|
move lines in _create_account_move_line method.
|
|
:param data_type: 'product', 'tax', ....
|
|
:param values: account move line values
|
|
:return: tuple() representing the data_type key
|
|
"""
|
|
if data_type == 'product':
|
|
return ('product',
|
|
values['partner_id'],
|
|
(values['product_id'], tuple(values['tax_ids'][0][2]), values['name']),
|
|
values['analytic_account_id'],
|
|
values['debit'] > 0)
|
|
elif data_type == 'tax':
|
|
return ('tax',
|
|
values['partner_id'],
|
|
values['tax_line_id'],
|
|
values['debit'] > 0)
|
|
elif data_type == 'counter_part':
|
|
return ('counter_part',
|
|
values['partner_id'],
|
|
values['account_id'],
|
|
values['debit'] > 0)
|
|
return False
|
|
|
|
def _action_create_invoice_line(self, line=False, invoice_id=False):
|
|
InvoiceLine = self.env['account.invoice.line']
|
|
inv_name = line.product_id.name_get()[0][1]
|
|
inv_line = {
|
|
'invoice_id': invoice_id,
|
|
'product_id': line.product_id.id,
|
|
'quantity': line.qty if self.amount_total >= 0 else -line.qty,
|
|
'account_analytic_id': self._prepare_analytic_account(line),
|
|
'name': inv_name,
|
|
}
|
|
# Oldlin trick
|
|
invoice_line = InvoiceLine.sudo().new(inv_line)
|
|
invoice_line._onchange_product_id()
|
|
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids
|
|
fiscal_position_id = line.order_id.fiscal_position_id
|
|
if fiscal_position_id:
|
|
invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids, line.product_id, line.order_id.partner_id)
|
|
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids
|
|
# We convert a new id object back to a dictionary to write to
|
|
# bridge between old and new api
|
|
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
|
|
inv_line.update(price_unit=line.price_unit, discount=line.discount, name=inv_name)
|
|
return InvoiceLine.sudo().create(inv_line)
|
|
|
|
def _create_account_move_line(self, session=None, move=None):
|
|
def _flatten_tax_and_children(taxes, group_done=None):
|
|
children = self.env['account.tax']
|
|
if group_done is None:
|
|
group_done = set()
|
|
for tax in taxes.filtered(lambda t: t.amount_type == 'group'):
|
|
if tax.id not in group_done:
|
|
group_done.add(tax.id)
|
|
children |= _flatten_tax_and_children(tax.children_tax_ids, group_done)
|
|
return taxes + children
|
|
|
|
# Tricky, via the workflow, we only have one id in the ids variable
|
|
"""Create a account move line of order grouped by products or not."""
|
|
IrProperty = self.env['ir.property']
|
|
ResPartner = self.env['res.partner']
|
|
|
|
if session and not all(session.id == order.session_id.id for order in self):
|
|
raise UserError(_('Selected orders do not have the same session!'))
|
|
|
|
grouped_data = {}
|
|
have_to_group_by = session and session.config_id.group_by or False
|
|
rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method
|
|
|
|
def add_anglosaxon_lines(grouped_data):
|
|
Product = self.env['product.product']
|
|
Analytic = self.env['account.analytic.account']
|
|
for product_key in list(grouped_data.keys()):
|
|
if product_key[0] == "product":
|
|
line = grouped_data[product_key][0]
|
|
product = Product.browse(line['product_id'])
|
|
# In the SO part, the entries will be inverted by function compute_invoice_totals
|
|
price_unit = self._get_pos_anglo_saxon_price_unit(product, line['partner_id'], line['quantity'])
|
|
account_analytic = Analytic.browse(line.get('analytic_account_id'))
|
|
res = Product._anglo_saxon_sale_move_lines(
|
|
line['name'], product, product.uom_id, line['quantity'], price_unit,
|
|
fiscal_position=order.fiscal_position_id,
|
|
account_analytic=account_analytic)
|
|
if res:
|
|
line1, line2 = res
|
|
line1 = Product._convert_prepared_anglosaxon_line(line1, order.partner_id)
|
|
insert_data('counter_part', {
|
|
'name': line1['name'],
|
|
'account_id': line1['account_id'],
|
|
'credit': line1['credit'] or 0.0,
|
|
'debit': line1['debit'] or 0.0,
|
|
'partner_id': line1['partner_id']
|
|
|
|
})
|
|
|
|
line2 = Product._convert_prepared_anglosaxon_line(line2, order.partner_id)
|
|
insert_data('counter_part', {
|
|
'name': line2['name'],
|
|
'account_id': line2['account_id'],
|
|
'credit': line2['credit'] or 0.0,
|
|
'debit': line2['debit'] or 0.0,
|
|
'partner_id': line2['partner_id']
|
|
})
|
|
|
|
for order in self.filtered(lambda o: not o.account_move or o.state == 'paid'):
|
|
current_company = order.sale_journal.company_id
|
|
account_def = IrProperty.get(
|
|
'property_account_receivable_id', 'res.partner')
|
|
order_account = order.partner_id.property_account_receivable_id.id or account_def and account_def.id
|
|
partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False
|
|
if move is None:
|
|
# Create an entry for the sale
|
|
journal_id = self.env['ir.config_parameter'].sudo().get_param(
|
|
'pos.closing.journal_id_%s' % current_company.id, default=order.sale_journal.id)
|
|
move = self._create_account_move(
|
|
order.session_id.start_at, order.name, int(journal_id), order.company_id.id)
|
|
|
|
def insert_data(data_type, values):
|
|
# if have_to_group_by:
|
|
values.update({
|
|
'partner_id': partner_id,
|
|
'move_id': move.id,
|
|
})
|
|
|
|
key = self._get_account_move_line_group_data_type_key(data_type, values)
|
|
if not key:
|
|
return
|
|
|
|
grouped_data.setdefault(key, [])
|
|
|
|
if have_to_group_by:
|
|
if not grouped_data[key]:
|
|
grouped_data[key].append(values)
|
|
else:
|
|
current_value = grouped_data[key][0]
|
|
current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
|
|
current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
|
|
current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
|
|
else:
|
|
grouped_data[key].append(values)
|
|
|
|
# because of the weird way the pos order is written, we need to make sure there is at least one line,
|
|
# because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
|
|
# are set inside the for loop)
|
|
# TOFIX: a deep refactoring of this method (and class!) is needed
|
|
# in order to get rid of this stupid hack
|
|
assert order.lines, _('The POS order must have lines when calling this method')
|
|
# Create an move for each order line
|
|
cur = order.pricelist_id.currency_id
|
|
for line in order.lines:
|
|
amount = line.price_subtotal
|
|
|
|
# Search for the income account
|
|
if line.product_id.property_account_income_id.id:
|
|
income_account = line.product_id.property_account_income_id.id
|
|
elif line.product_id.categ_id.property_account_income_categ_id.id:
|
|
income_account = line.product_id.categ_id.property_account_income_categ_id.id
|
|
else:
|
|
raise UserError(_('Please define income '
|
|
'account for this product: "%s" (id:%d).')
|
|
% (line.product_id.name, line.product_id.id))
|
|
|
|
name = line.product_id.name
|
|
if line.notice:
|
|
# add discount reason in move
|
|
name = name + ' (' + line.notice + ')'
|
|
|
|
# Create a move for the line for the order line
|
|
# Just like for invoices, a group of taxes must be present on this base line
|
|
# As well as its children
|
|
base_line_tax_ids = _flatten_tax_and_children(line.tax_ids_after_fiscal_position).filtered(lambda tax: tax.type_tax_use in ['sale', 'none'])
|
|
insert_data('product', {
|
|
'name': name,
|
|
'quantity': line.qty,
|
|
'product_id': line.product_id.id,
|
|
'account_id': income_account,
|
|
'analytic_account_id': self._prepare_analytic_account(line),
|
|
'credit': ((amount > 0) and amount) or 0.0,
|
|
'debit': ((amount < 0) and -amount) or 0.0,
|
|
'tax_ids': [(6, 0, base_line_tax_ids.ids)],
|
|
'partner_id': partner_id
|
|
})
|
|
|
|
# Create the tax lines
|
|
taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id)
|
|
if not taxes:
|
|
continue
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
for tax in taxes.compute_all(price, cur, line.qty)['taxes']:
|
|
insert_data('tax', {
|
|
'name': _('Tax') + ' ' + tax['name'],
|
|
'product_id': line.product_id.id,
|
|
'quantity': line.qty,
|
|
'account_id': tax['account_id'] or income_account,
|
|
'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0,
|
|
'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0,
|
|
'tax_line_id': tax['id'],
|
|
'partner_id': partner_id
|
|
})
|
|
|
|
# round tax lines per order
|
|
if rounding_method == 'round_globally':
|
|
for group_key, group_value in grouped_data.items():
|
|
if group_key[0] == 'tax':
|
|
for line in group_value:
|
|
line['credit'] = cur.round(line['credit'])
|
|
line['debit'] = cur.round(line['debit'])
|
|
|
|
# counterpart
|
|
insert_data('counter_part', {
|
|
'name': _("Trade Receivables"), # order.name,
|
|
'account_id': order_account,
|
|
'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
|
|
'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
|
|
'partner_id': partner_id
|
|
})
|
|
|
|
order.write({'state': 'done', 'account_move': move.id})
|
|
|
|
if self and order.company_id.anglo_saxon_accounting:
|
|
add_anglosaxon_lines(grouped_data)
|
|
|
|
all_lines = []
|
|
for group_key, group_data in grouped_data.items():
|
|
for value in group_data:
|
|
all_lines.append((0, 0, value),)
|
|
if move: # In case no order was changed
|
|
move.sudo().write({'line_ids': all_lines})
|
|
move.sudo().post()
|
|
return True
|
|
|
|
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
|
|
price_unit = product._get_anglo_saxon_price_unit()
|
|
if product._get_invoice_policy() == "delivery":
|
|
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
|
|
.mapped('picking_id.move_lines')\
|
|
.filtered(lambda m: m.product_id.id == product.id)\
|
|
.sorted(lambda x: x.date)
|
|
average_price_unit = product._compute_average_price(0, quantity, moves)
|
|
price_unit = average_price_unit or price_unit
|
|
# In the SO part, the entries will be inverted by function compute_invoice_totals
|
|
return - price_unit
|
|
|
|
def _reconcile_payments(self):
|
|
for order in self:
|
|
aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids
|
|
aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id)
|
|
|
|
# Reconcile returns first
|
|
# to avoid mixing up the credit of a payment and the credit of a return
|
|
# in the receivable account
|
|
aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit))
|
|
try:
|
|
aml_returns.reconcile()
|
|
(aml - aml_returns).reconcile()
|
|
except Exception:
|
|
# There might be unexpected situations where the automatic reconciliation won't
|
|
# work. We don't want the user to be blocked because of this, since the automatic
|
|
# reconciliation is introduced for convenience, not for mandatory accounting
|
|
# reasons.
|
|
# It may be interesting to have the Traceback logged anyway
|
|
# for debugging and support purposes
|
|
_logger.exception('Reconciliation did not work for order %s', order.name)
|
|
|
|
def _default_session(self):
|
|
return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
|
|
|
|
def _default_pricelist(self):
|
|
return self._default_session().config_id.pricelist_id
|
|
|
|
name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/')
|
|
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id)
|
|
date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now)
|
|
user_id = fields.Many2one(
|
|
comodel_name='res.users', string='Salesman',
|
|
help="Person who uses the cash register. It can be a reliever, a student or an interim employee.",
|
|
default=lambda self: self.env.uid,
|
|
states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]},
|
|
)
|
|
amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0)
|
|
amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0)
|
|
amount_paid = fields.Float(compute='_compute_amount_all', string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0)
|
|
amount_return = fields.Float(compute='_compute_amount_all', string='Returned', digits=0)
|
|
lines = fields.One2many('pos.order.line', 'order_id', string='Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True)
|
|
statement_ids = fields.One2many('account.bank.statement.line', 'pos_statement_id', string='Payments', states={'draft': [('readonly', False)]}, readonly=True)
|
|
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, states={
|
|
'draft': [('readonly', False)]}, readonly=True, default=_default_pricelist)
|
|
partner_id = fields.Many2one('res.partner', string='Customer', change_default=True, index=True, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]})
|
|
sequence_number = fields.Integer(string='Sequence Number', help='A session-unique sequence number for the order', default=1)
|
|
|
|
session_id = fields.Many2one(
|
|
'pos.session', string='Session', required=True, index=True,
|
|
domain="[('state', '=', 'opened')]", states={'draft': [('readonly', False)]},
|
|
readonly=True, default=_default_session)
|
|
config_id = fields.Many2one('pos.config', related='session_id.config_id', string="Point of Sale")
|
|
state = fields.Selection(
|
|
[('draft', 'New'), ('cancel', 'Cancelled'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced')],
|
|
'Status', readonly=True, copy=False, default='draft')
|
|
|
|
invoice_id = fields.Many2one('account.invoice', string='Invoice', copy=False)
|
|
account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False)
|
|
picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False)
|
|
picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type")
|
|
location_id = fields.Many2one(
|
|
comodel_name='stock.location',
|
|
related='session_id.config_id.stock_location_id',
|
|
string="Location", store=True,
|
|
readonly=True,
|
|
)
|
|
note = fields.Text(string='Internal Notes')
|
|
nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0)
|
|
pos_reference = fields.Char(string='Receipt Ref', readonly=True, copy=False)
|
|
sale_journal = fields.Many2one('account.journal', related='session_id.config_id.journal_id', string='Sales Journal', store=True, readonly=True)
|
|
fiscal_position_id = fields.Many2one(
|
|
comodel_name='account.fiscal.position', string='Fiscal Position',
|
|
default=lambda self: self._default_session().config_id.default_fiscal_position_id,
|
|
readonly=True,
|
|
states={'draft': [('readonly', False)]},
|
|
)
|
|
|
|
@api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount')
|
|
def _compute_amount_all(self):
|
|
for order in self:
|
|
order.amount_paid = order.amount_return = order.amount_tax = 0.0
|
|
currency = order.pricelist_id.currency_id
|
|
order.amount_paid = sum(payment.amount for payment in order.statement_ids)
|
|
order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.statement_ids)
|
|
order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
|
|
amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines))
|
|
order.amount_total = order.amount_tax + amount_untaxed
|
|
|
|
@api.onchange('partner_id')
|
|
def _onchange_partner_id(self):
|
|
if self.partner_id:
|
|
self.pricelist = self.partner_id.property_product_pricelist.id
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
res = super(PosOrder, self).write(vals)
|
|
Partner = self.env['res.partner']
|
|
# If you change the partner of the PoS order, change also the partner of the associated bank statement lines
|
|
if 'partner_id' in vals:
|
|
for order in self:
|
|
partner_id = False
|
|
if order.invoice_id:
|
|
raise UserError(_("You cannot change the partner of a POS order for which an invoice has already been issued."))
|
|
if vals['partner_id']:
|
|
partner = Partner.browse(vals['partner_id'])
|
|
partner_id = Partner._find_accounting_partner(partner).id
|
|
order.statement_ids.write({'partner_id': partner_id})
|
|
return res
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
for pos_order in self.filtered(lambda pos_order: pos_order.state not in ['draft', 'cancel']):
|
|
raise UserError(_('In order to delete a sale, it must be new or cancelled.'))
|
|
return super(PosOrder, self).unlink()
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
if values.get('session_id'):
|
|
# set name based on the sequence specified on the config
|
|
session = self.env['pos.session'].browse(values['session_id'])
|
|
values['name'] = session.config_id.sequence_id._next()
|
|
values.setdefault('pricelist_id', session.config_id.pricelist_id.id)
|
|
else:
|
|
# fallback on any pos.order sequence
|
|
values['name'] = self.env['ir.sequence'].next_by_code('pos.order')
|
|
return super(PosOrder, self).create(values)
|
|
|
|
@api.multi
|
|
def action_view_invoice(self):
|
|
return {
|
|
'name': _('Customer Invoice'),
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('account.invoice_form').id,
|
|
'res_model': 'account.invoice',
|
|
'context': "{'type':'out_invoice'}",
|
|
'type': 'ir.actions.act_window',
|
|
'res_id': self.invoice_id.id,
|
|
}
|
|
|
|
@api.multi
|
|
def action_pos_order_paid(self):
|
|
if not self.test_paid():
|
|
raise UserError(_("Order is not paid."))
|
|
self.write({'state': 'paid'})
|
|
return self.create_picking()
|
|
|
|
@api.multi
|
|
def action_pos_order_invoice(self):
|
|
Invoice = self.env['account.invoice']
|
|
|
|
for order in self:
|
|
# Force company for all SUPERUSER_ID action
|
|
local_context = dict(self.env.context, force_company=order.company_id.id, company_id=order.company_id.id)
|
|
if order.invoice_id:
|
|
Invoice += order.invoice_id
|
|
continue
|
|
|
|
if not order.partner_id:
|
|
raise UserError(_('Please provide a partner for the sale.'))
|
|
|
|
invoice = Invoice.new(order._prepare_invoice())
|
|
invoice._onchange_partner_id()
|
|
invoice.fiscal_position_id = order.fiscal_position_id
|
|
|
|
inv = invoice._convert_to_write({name: invoice[name] for name in invoice._cache})
|
|
new_invoice = Invoice.with_context(local_context).sudo().create(inv)
|
|
message = _("This invoice has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name)
|
|
new_invoice.message_post(body=message)
|
|
order.write({'invoice_id': new_invoice.id, 'state': 'invoiced'})
|
|
Invoice += new_invoice
|
|
|
|
for line in order.lines:
|
|
self.with_context(local_context)._action_create_invoice_line(line, new_invoice.id)
|
|
|
|
new_invoice.with_context(local_context).sudo().compute_taxes()
|
|
order.sudo().write({'state': 'invoiced'})
|
|
|
|
if not Invoice:
|
|
return {}
|
|
|
|
return {
|
|
'name': _('Customer Invoice'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('account.invoice_form').id,
|
|
'res_model': 'account.invoice',
|
|
'context': "{'type':'out_invoice'}",
|
|
'type': 'ir.actions.act_window',
|
|
'nodestroy': True,
|
|
'target': 'current',
|
|
'res_id': Invoice and Invoice.ids[0] or False,
|
|
}
|
|
|
|
# this method is unused, and so is the state 'cancel'
|
|
@api.multi
|
|
def action_pos_order_cancel(self):
|
|
return self.write({'state': 'cancel'})
|
|
|
|
@api.multi
|
|
def action_pos_order_done(self):
|
|
return self._create_account_move_line()
|
|
|
|
@api.model
|
|
def create_from_ui(self, orders):
|
|
# Keep only new orders
|
|
submitted_references = [o['data']['name'] for o in orders]
|
|
pos_order = self.search([('pos_reference', 'in', submitted_references)])
|
|
existing_orders = pos_order.read(['pos_reference'])
|
|
existing_references = set([o['pos_reference'] for o in existing_orders])
|
|
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
|
|
order_ids = []
|
|
|
|
for tmp_order in orders_to_save:
|
|
to_invoice = tmp_order['to_invoice']
|
|
order = tmp_order['data']
|
|
if to_invoice:
|
|
self._match_payment_to_invoice(order)
|
|
pos_order = self._process_order(order)
|
|
order_ids.append(pos_order.id)
|
|
|
|
try:
|
|
pos_order.action_pos_order_paid()
|
|
except psycopg2.OperationalError:
|
|
# do not hide transactional errors, the order(s) won't be saved!
|
|
raise
|
|
except Exception as e:
|
|
_logger.error('Could not fully process the POS Order: %s', tools.ustr(e))
|
|
|
|
if to_invoice:
|
|
pos_order.action_pos_order_invoice()
|
|
pos_order.invoice_id.sudo().action_invoice_open()
|
|
pos_order.account_move = pos_order.invoice_id.move_id
|
|
return order_ids
|
|
|
|
def test_paid(self):
|
|
"""A Point of Sale is paid when the sum
|
|
@return: True
|
|
"""
|
|
for order in self:
|
|
if order.lines and not order.amount_total:
|
|
continue
|
|
if (not order.lines) or (not order.statement_ids) or (abs(order.amount_total - order.amount_paid) > 0.00001):
|
|
return False
|
|
return True
|
|
|
|
def create_picking(self):
|
|
"""Create a picking for each order and validate it."""
|
|
Picking = self.env['stock.picking']
|
|
Move = self.env['stock.move']
|
|
StockWarehouse = self.env['stock.warehouse']
|
|
for order in self:
|
|
if not order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']):
|
|
continue
|
|
address = order.partner_id.address_get(['delivery']) or {}
|
|
picking_type = order.picking_type_id
|
|
return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id
|
|
order_picking = Picking
|
|
return_picking = Picking
|
|
moves = Move
|
|
location_id = order.location_id.id
|
|
if order.partner_id:
|
|
destination_id = order.partner_id.property_stock_customer.id
|
|
else:
|
|
if (not picking_type) or (not picking_type.default_location_dest_id):
|
|
customerloc, supplierloc = StockWarehouse._get_partner_locations()
|
|
destination_id = customerloc.id
|
|
else:
|
|
destination_id = picking_type.default_location_dest_id.id
|
|
|
|
if picking_type:
|
|
message = _("This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id=%d>%s</a>") % (order.id, order.name)
|
|
picking_vals = {
|
|
'origin': order.name,
|
|
'partner_id': address.get('delivery', False),
|
|
'date_done': order.date_order,
|
|
'picking_type_id': picking_type.id,
|
|
'company_id': order.company_id.id,
|
|
'move_type': 'direct',
|
|
'note': order.note or "",
|
|
'location_id': location_id,
|
|
'location_dest_id': destination_id,
|
|
}
|
|
pos_qty = any([x.qty > 0 for x in order.lines if x.product_id.type in ['product', 'consu']])
|
|
if pos_qty:
|
|
order_picking = Picking.create(picking_vals.copy())
|
|
order_picking.message_post(body=message)
|
|
neg_qty = any([x.qty < 0 for x in order.lines if x.product_id.type in ['product', 'consu']])
|
|
if neg_qty:
|
|
return_vals = picking_vals.copy()
|
|
return_vals.update({
|
|
'location_id': destination_id,
|
|
'location_dest_id': return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
|
|
'picking_type_id': return_pick_type.id
|
|
})
|
|
return_picking = Picking.create(return_vals)
|
|
return_picking.message_post(body=message)
|
|
|
|
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_rounding=l.product_id.uom_id.rounding)):
|
|
moves |= Move.create({
|
|
'name': line.name,
|
|
'product_uom': line.product_id.uom_id.id,
|
|
'picking_id': order_picking.id if line.qty >= 0 else return_picking.id,
|
|
'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id,
|
|
'product_id': line.product_id.id,
|
|
'product_uom_qty': abs(line.qty),
|
|
'state': 'draft',
|
|
'location_id': location_id if line.qty >= 0 else destination_id,
|
|
'location_dest_id': destination_id if line.qty >= 0 else return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id,
|
|
})
|
|
|
|
# prefer associating the regular order picking, not the return
|
|
order.write({'picking_id': order_picking.id or return_picking.id})
|
|
|
|
if return_picking:
|
|
order._force_picking_done(return_picking)
|
|
if order_picking:
|
|
order._force_picking_done(order_picking)
|
|
|
|
# when the pos.config has no picking_type_id set only the moves will be created
|
|
if moves and not return_picking and not order_picking:
|
|
moves._action_assign()
|
|
moves.filtered(lambda m: m.state in ['confirmed', 'waiting'])._force_assign()
|
|
moves.filtered(lambda m: m.product_id.tracking == 'none')._action_done()
|
|
|
|
return True
|
|
|
|
def _force_picking_done(self, picking):
|
|
"""Force picking in order to be set as done."""
|
|
self.ensure_one()
|
|
picking.action_assign()
|
|
picking.force_assign()
|
|
wrong_lots = self.set_pack_operation_lot(picking)
|
|
if not wrong_lots:
|
|
picking.action_done()
|
|
|
|
def set_pack_operation_lot(self, picking=None):
|
|
"""Set Serial/Lot number in pack operations to mark the pack operation done."""
|
|
|
|
StockProductionLot = self.env['stock.production.lot']
|
|
PosPackOperationLot = self.env['pos.pack.operation.lot']
|
|
has_wrong_lots = False
|
|
for order in self:
|
|
for move in (picking or self.picking_id).move_lines:
|
|
picking_type = (picking or self.picking_id).picking_type_id
|
|
lots_necessary = True
|
|
if picking_type:
|
|
lots_necessary = picking_type and picking_type.use_existing_lots
|
|
qty = 0
|
|
qty_done = 0
|
|
pack_lots = []
|
|
pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', move.product_id.id)])
|
|
pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots]
|
|
|
|
if pack_lot_names and lots_necessary:
|
|
for lot_name in list(set(pack_lot_names)):
|
|
stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', move.product_id.id)])
|
|
if stock_production_lot:
|
|
if stock_production_lot.product_id.tracking == 'lot':
|
|
# if a lot nr is set through the frontend it will refer to the full quantity
|
|
qty = move.product_uom_qty
|
|
else: # serial numbers
|
|
qty = 1.0
|
|
qty_done += qty
|
|
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
|
|
else:
|
|
has_wrong_lots = True
|
|
elif move.product_id.tracking == 'none' or not lots_necessary:
|
|
qty_done = move.product_uom_qty
|
|
else:
|
|
has_wrong_lots = True
|
|
for pack_lot in pack_lots:
|
|
lot_id, qty = pack_lot['lot_id'], pack_lot['qty']
|
|
self.env['stock.move.line'].create({
|
|
'move_id': move.id,
|
|
'product_id': move.product_id.id,
|
|
'product_uom_id': move.product_uom.id,
|
|
'qty_done': qty,
|
|
'location_id': move.location_id.id,
|
|
'location_dest_id': move.location_dest_id.id,
|
|
'lot_id': lot_id,
|
|
})
|
|
if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding):
|
|
move.quantity_done = qty_done
|
|
return has_wrong_lots
|
|
|
|
def _prepare_bank_statement_line_payment_values(self, data):
|
|
"""Create a new payment for the order"""
|
|
args = {
|
|
'amount': data['amount'],
|
|
'date': data.get('payment_date', fields.Date.context_today(self)),
|
|
'name': self.name + ': ' + (data.get('payment_name', '') or ''),
|
|
'partner_id': self.env["res.partner"]._find_accounting_partner(self.partner_id).id or False,
|
|
}
|
|
|
|
journal_id = data.get('journal', False)
|
|
statement_id = data.get('statement_id', False)
|
|
assert journal_id or statement_id, "No statement_id or journal_id passed to the method!"
|
|
|
|
journal = self.env['account.journal'].browse(journal_id)
|
|
# use the company of the journal and not of the current user
|
|
company_cxt = dict(self.env.context, force_company=journal.company_id.id)
|
|
account_def = self.env['ir.property'].with_context(company_cxt).get('property_account_receivable_id', 'res.partner')
|
|
args['account_id'] = (self.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False
|
|
|
|
if not args['account_id']:
|
|
if not args['partner_id']:
|
|
msg = _('There is no receivable account defined to make payment.')
|
|
else:
|
|
msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (
|
|
self.partner_id.name, self.partner_id.id,)
|
|
raise UserError(msg)
|
|
|
|
context = dict(self.env.context)
|
|
context.pop('pos_session_id', False)
|
|
for statement in self.session_id.statement_ids:
|
|
if statement.id == statement_id:
|
|
journal_id = statement.journal_id.id
|
|
break
|
|
elif statement.journal_id.id == journal_id:
|
|
statement_id = statement.id
|
|
break
|
|
if not statement_id:
|
|
raise UserError(_('You have to open at least one cashbox.'))
|
|
|
|
args.update({
|
|
'statement_id': statement_id,
|
|
'pos_statement_id': self.id,
|
|
'journal_id': journal_id,
|
|
'ref': self.session_id.name,
|
|
})
|
|
|
|
return args
|
|
|
|
def add_payment(self, data):
|
|
"""Create a new payment for the order"""
|
|
args = self._prepare_bank_statement_line_payment_values(data)
|
|
context = dict(self.env.context)
|
|
context.pop('pos_session_id', False)
|
|
self.env['account.bank.statement.line'].with_context(context).create(args)
|
|
return args.get('statement_id', False)
|
|
|
|
@api.multi
|
|
def refund(self):
|
|
"""Create a copy of order for refund order"""
|
|
PosOrder = self.env['pos.order']
|
|
current_session = self.env['pos.session'].search([('state', '!=', 'closed'), ('user_id', '=', self.env.uid)], limit=1)
|
|
if not current_session:
|
|
raise UserError(_('To return product(s), you need to open a session that will be used to register the refund.'))
|
|
for order in self:
|
|
clone = order.copy({
|
|
# ot used, name forced by create
|
|
'name': order.name + _(' REFUND'),
|
|
'session_id': current_session.id,
|
|
'date_order': fields.Datetime.now(),
|
|
'pos_reference': order.pos_reference,
|
|
'lines': False,
|
|
})
|
|
for line in order.lines:
|
|
clone_line = line.copy({
|
|
# required=True, copy=False
|
|
'name': line.name + _(' REFUND'),
|
|
'order_id': clone.id,
|
|
'qty': -line.qty,
|
|
})
|
|
PosOrder += clone
|
|
|
|
return {
|
|
'name': _('Return Products'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'pos.order',
|
|
'res_id': PosOrder.ids[0],
|
|
'view_id': False,
|
|
'context': self.env.context,
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'current',
|
|
}
|
|
|
|
|
|
class PosOrderLine(models.Model):
|
|
_name = "pos.order.line"
|
|
_description = "Lines of Point of Sale Orders"
|
|
_rec_name = "product_id"
|
|
|
|
def _order_line_fields(self, line, session_id=None):
|
|
if line and 'name' not in line[2]:
|
|
session = self.env['pos.session'].browse(session_id).exists() if session_id else None
|
|
if session and session.config_id.sequence_line_id:
|
|
# set name based on the sequence specified on the config
|
|
line[2]['name'] = session.config_id.sequence_line_id._next()
|
|
else:
|
|
# fallback on any pos.order.line sequence
|
|
line[2]['name'] = self.env['ir.sequence'].next_by_code('pos.order.line')
|
|
|
|
if line and 'tax_ids' not in line[2]:
|
|
product = self.env['product.product'].browse(line[2]['product_id'])
|
|
line[2]['tax_ids'] = [(6, 0, [x.id for x in product.taxes_id])]
|
|
return line
|
|
|
|
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id)
|
|
name = fields.Char(string='Line No', required=True, copy=False)
|
|
notice = fields.Char(string='Discount Notice')
|
|
product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], required=True, change_default=True)
|
|
price_unit = fields.Float(string='Unit Price', digits=0)
|
|
qty = fields.Float('Quantity', digits=dp.get_precision('Product Unit of Measure'), default=1)
|
|
price_subtotal = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal w/o Tax')
|
|
price_subtotal_incl = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal')
|
|
discount = fields.Float(string='Discount (%)', digits=0, default=0.0)
|
|
order_id = fields.Many2one('pos.order', string='Order Ref', ondelete='cascade')
|
|
create_date = fields.Datetime(string='Creation Date', readonly=True)
|
|
tax_ids = fields.Many2many('account.tax', string='Taxes', readonly=True)
|
|
tax_ids_after_fiscal_position = fields.Many2many('account.tax', compute='_get_tax_ids_after_fiscal_position', string='Taxes')
|
|
pack_lot_ids = fields.One2many('pos.pack.operation.lot', 'pos_order_line_id', string='Lot/serial Number')
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
if values.get('order_id') and not values.get('name'):
|
|
# set name based on the sequence specified on the config
|
|
config_id = self.order_id.browse(values['order_id']).session_id.config_id.id
|
|
# HACK: sequence created in the same transaction as the config
|
|
# cf TODO master is pos.config create
|
|
# remove me saas-15
|
|
self.env.cr.execute("""
|
|
SELECT s.id
|
|
FROM ir_sequence s
|
|
JOIN pos_config c
|
|
ON s.create_date=c.create_date
|
|
WHERE c.id = %s
|
|
AND s.code = 'pos.order.line'
|
|
LIMIT 1
|
|
""", (config_id,))
|
|
sequence = self.env.cr.fetchone()
|
|
if sequence:
|
|
values['name'] = self.env['ir.sequence'].browse(sequence[0])._next()
|
|
if not values.get('name'):
|
|
# fallback on any pos.order sequence
|
|
values['name'] = self.env['ir.sequence'].next_by_code('pos.order.line')
|
|
return super(PosOrderLine, self).create(values)
|
|
|
|
@api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id')
|
|
def _compute_amount_line_all(self):
|
|
for line in self:
|
|
fpos = line.order_id.fiscal_position_id
|
|
tax_ids_after_fiscal_position = fpos.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id) if fpos else line.tax_ids
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
taxes = tax_ids_after_fiscal_position.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id)
|
|
line.update({
|
|
'price_subtotal_incl': taxes['total_included'],
|
|
'price_subtotal': taxes['total_excluded'],
|
|
})
|
|
|
|
@api.onchange('product_id')
|
|
def _onchange_product_id(self):
|
|
if self.product_id:
|
|
if not self.order_id.pricelist_id:
|
|
raise UserError(
|
|
_('You have to select a pricelist in the sale form !\n'
|
|
'Please set one before choosing a product.'))
|
|
price = self.order_id.pricelist_id.get_product_price(
|
|
self.product_id, self.qty or 1.0, self.order_id.partner_id)
|
|
self._onchange_qty()
|
|
self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id)
|
|
fpos = self.order_id.fiscal_position_id
|
|
tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids, self.product_id, self.order_id.partner_id) if fpos else self.tax_ids
|
|
self.price_unit = self.env['account.tax']._fix_tax_included_price_company(price, self.product_id.taxes_id, tax_ids_after_fiscal_position, self.company_id)
|
|
|
|
@api.onchange('qty', 'discount', 'price_unit', 'tax_ids')
|
|
def _onchange_qty(self):
|
|
if self.product_id:
|
|
if not self.order_id.pricelist_id:
|
|
raise UserError(_('You have to select a pricelist in the sale form !'))
|
|
price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
|
|
self.price_subtotal = self.price_subtotal_incl = price * self.qty
|
|
if (self.product_id.taxes_id):
|
|
taxes = self.product_id.taxes_id.compute_all(price, self.order_id.pricelist_id.currency_id, self.qty, product=self.product_id, partner=False)
|
|
self.price_subtotal = taxes['total_excluded']
|
|
self.price_subtotal_incl = taxes['total_included']
|
|
|
|
@api.multi
|
|
def _get_tax_ids_after_fiscal_position(self):
|
|
for line in self:
|
|
line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id)
|
|
|
|
|
|
class PosOrderLineLot(models.Model):
|
|
_name = "pos.pack.operation.lot"
|
|
_description = "Specify product lot/serial number in pos order line"
|
|
|
|
pos_order_line_id = fields.Many2one('pos.order.line')
|
|
order_id = fields.Many2one('pos.order', related="pos_order_line_id.order_id")
|
|
lot_name = fields.Char('Lot Name')
|
|
product_id = fields.Many2one('product.product', related='pos_order_line_id.product_id')
|
|
|
|
|
|
class ReportSaleDetails(models.AbstractModel):
|
|
|
|
_name = 'report.point_of_sale.report_saledetails'
|
|
|
|
|
|
@api.model
|
|
def get_sale_details(self, date_start=False, date_stop=False, configs=False):
|
|
""" Serialise the orders of the day information
|
|
|
|
params: date_start, date_stop string representing the datetime of order
|
|
"""
|
|
if not configs:
|
|
configs = self.env['pos.config'].search([])
|
|
|
|
user_tz = pytz.timezone(self.env.context.get('tz') or self.env.user.tz or 'UTC')
|
|
today = user_tz.localize(fields.Datetime.from_string(fields.Date.context_today(self)))
|
|
today = today.astimezone(pytz.timezone('UTC'))
|
|
if date_start:
|
|
date_start = fields.Datetime.from_string(date_start)
|
|
else:
|
|
# start by default today 00:00:00
|
|
date_start = today
|
|
|
|
if date_stop:
|
|
# set time to 23:59:59
|
|
date_stop = fields.Datetime.from_string(date_stop)
|
|
else:
|
|
# stop by default today 23:59:59
|
|
date_stop = today + timedelta(days=1, seconds=-1)
|
|
|
|
# avoid a date_stop smaller than date_start
|
|
date_stop = max(date_stop, date_start)
|
|
|
|
date_start = fields.Datetime.to_string(date_start)
|
|
date_stop = fields.Datetime.to_string(date_stop)
|
|
|
|
orders = self.env['pos.order'].search([
|
|
('date_order', '>=', date_start),
|
|
('date_order', '<=', date_stop),
|
|
('state', 'in', ['paid','invoiced','done']),
|
|
('config_id', 'in', configs.ids)])
|
|
|
|
user_currency = self.env.user.company_id.currency_id
|
|
|
|
total = 0.0
|
|
products_sold = {}
|
|
taxes = {}
|
|
for order in orders:
|
|
if user_currency != order.pricelist_id.currency_id:
|
|
total += order.pricelist_id.currency_id.compute(order.amount_total, user_currency)
|
|
else:
|
|
total += order.amount_total
|
|
currency = order.session_id.currency_id
|
|
|
|
for line in order.lines:
|
|
key = (line.product_id, line.price_unit, line.discount)
|
|
products_sold.setdefault(key, 0.0)
|
|
products_sold[key] += line.qty
|
|
|
|
if line.tax_ids_after_fiscal_position:
|
|
line_taxes = line.tax_ids_after_fiscal_position.compute_all(line.price_unit * (1-(line.discount or 0.0)/100.0), currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
|
|
for tax in line_taxes['taxes']:
|
|
taxes.setdefault(tax['id'], {'name': tax['name'], 'tax_amount':0.0, 'base_amount':0.0})
|
|
taxes[tax['id']]['tax_amount'] += tax['amount']
|
|
taxes[tax['id']]['base_amount'] += tax['base']
|
|
else:
|
|
taxes.setdefault(0, {'name': _('No Taxes'), 'tax_amount':0.0, 'base_amount':0.0})
|
|
taxes[0]['base_amount'] += line.price_subtotal_incl
|
|
|
|
st_line_ids = self.env["account.bank.statement.line"].search([('pos_statement_id', 'in', orders.ids)]).ids
|
|
if st_line_ids:
|
|
self.env.cr.execute("""
|
|
SELECT aj.name, sum(amount) total
|
|
FROM account_bank_statement_line AS absl,
|
|
account_bank_statement AS abs,
|
|
account_journal AS aj
|
|
WHERE absl.statement_id = abs.id
|
|
AND abs.journal_id = aj.id
|
|
AND absl.id IN %s
|
|
GROUP BY aj.name
|
|
""", (tuple(st_line_ids),))
|
|
payments = self.env.cr.dictfetchall()
|
|
else:
|
|
payments = []
|
|
|
|
return {
|
|
'currency_precision': user_currency.decimal_places,
|
|
'total_paid': user_currency.round(total),
|
|
'payments': payments,
|
|
'company_name': self.env.user.company_id.name,
|
|
'taxes': list(taxes.values()),
|
|
'products': sorted([{
|
|
'product_id': product.id,
|
|
'product_name': product.name,
|
|
'code': product.default_code,
|
|
'quantity': qty,
|
|
'price_unit': price_unit,
|
|
'discount': discount,
|
|
'uom': product.uom_id.name
|
|
} for (product, price_unit, discount), qty in products_sold.items()], key=lambda l: l['product_name'])
|
|
}
|
|
|
|
@api.multi
|
|
def get_report_values(self, docids, data=None):
|
|
data = dict(data or {})
|
|
configs = self.env['pos.config'].browse(data['config_ids'])
|
|
data.update(self.get_sale_details(data['date_start'], data['date_stop'], configs))
|
|
return data
|