trytonpsk-sale_shop/sale.py

1507 lines
56 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import logging
from decimal import Decimal
from datetime import date, timedelta
import calendar
from sql.aggregate import Sum
from sql.functions import Extract
from sql.conditionals import Case
from sql.operators import In
from trytond.model import fields, Unique, ModelView
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Or, Not
from trytond.report import Report
from trytond.wizard import (
Wizard, StateView, StateAction, StateReport, Button, StateTransition
)
from trytond.i18n import gettext
from .exceptions import (
SaleMissingSequenceError, ShopUserError, SaleWriteError, PartyMissingAccount
)
from trytond.modules.sale.exceptions import SaleValidationError
from trytond.model.exceptions import AccessError
from trytond.modules.product import round_price
logger = logging.getLogger(__name__)
_ZERO = Decimal('0.00')
# domain=[
# ('id', 'in', Eval('context', {}).get('shops', [])),
# ],
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
shop = fields.Many2One('sale.shop', 'Shop', required=True,
states={
'readonly': Or(Bool(Eval('number')), Bool(Eval('lines'))),
}, depends=['number', 'lines'])
shop_address = fields.Function(fields.Many2One('party.address',
'Shop Address'), 'on_change_with_shop_address')
source = fields.Many2One('sale.source', 'Source')
payments = fields.One2Many('account.statement.line', 'sale', 'Payments')
paid_amount = fields.Function(fields.Numeric('Paid Amount', digits=(16, 2)),
'get_paid_amount')
residual_amount = fields.Function(fields.Numeric('Residual Amount',
digits=(16, 2), readonly=True), 'get_residual_amount')
@classmethod
def __setup__(cls):
super(Sale, cls).__setup__()
cls._buttons.update({
'wizard_sale_payment': {
'invisible': Eval('state').in_(['done', 'transferred']),
'readonly': Not(Bool(Eval('lines'))),
},
})
cls.state_string = super(Sale, cls).state.translated('state')
t = cls.__table__()
cls._sql_constraints.extend([
('number_uniq', Unique(t, t.shop, t.number),
'There is another sale with the same number.\n'
'The number of the sale must be unique!')
])
shipment_addr_domain = cls.shipment_address.domain[:]
if shipment_addr_domain:
cls.shipment_address.domain = [
'OR',
shipment_addr_domain,
[('id', '=', Eval('shop_address', 0))],
]
else:
cls.shipment_address.domain = [('id', '=', Eval('shop_address'))]
cls.shipment_address.depends.append('shop_address')
cls.currency.states['readonly'] = Eval('state') != 'draft'
cls.currency.depends.append('shop')
@staticmethod
def default_company():
User = Pool().get('res.user')
user = User(Transaction().user)
return user.shop.company.id if user.shop else \
Transaction().context.get('company')
@fields.depends('company')
def on_change_company(self):
User = Pool().get('res.user')
user = User(Transaction().user)
if not user.shop:
super(Sale, self).on_change_company()
else:
self.invoice_method = user.shop.sale_invoice_method
self.shipment_method = user.shop.sale_shipment_method
self.payment_term = user.shop.payment_term.id
@staticmethod
def default_shop():
User = Pool().get('res.user')
user = User(Transaction().user)
return user.shop.id if user.shop else None
@staticmethod
def default_description():
config = Pool().get('sale.configuration')(1)
if config.default_description:
return config.default_description
@staticmethod
def default_warehouse():
User = Pool().get('res.user')
user = User(Transaction().user)
if user.shop:
return user.shop.warehouse.id
else:
Location = Pool().get('stock.location')
return Location.search([('type', '=', 'warehouse')], limit=1)[0].id
@staticmethod
def default_price_list():
User = Pool().get('res.user')
user = User(Transaction().user)
return user.shop.price_list.id if user.shop else None
@staticmethod
def default_shop_address():
User = Pool().get('res.user')
user = User(Transaction().user)
return (user.shop and user.shop.address and user.shop.address.id or None)
@classmethod
def copy(cls, sales, default=None):
if default is None:
default = {}
default['payments'] = None
default['source'] = None
return super(Sale, cls).copy(sales, default)
@fields.depends('shop', 'party')
def on_change_shop(self):
if not self.shop:
return
for fname in ('company', 'warehouse', 'currency', 'payment_term'):
fvalue = getattr(self.shop, fname)
if fvalue:
setattr(self, fname, fvalue.id)
if ((not self.party or not self.party.sale_price_list)
and self.shop.price_list):
self.price_list = self.shop.price_list.id
if self.shop.sale_invoice_method:
self.invoice_method = self.shop.sale_invoice_method
if self.shop.sale_shipment_method:
self.shipment_method = self.shop.sale_shipment_method
@fields.depends('shop')
def on_change_with_shop_address(self, name=None):
return (self.shop and self.shop.address and
self.shop.address.id or None)
@fields.depends('shop', 'party')
def on_change_party(self):
super(Sale, self).on_change_party()
if self.shop:
if not self.price_list and self.invoice_address:
self.price_list = self.shop.price_list.id
self.price_list.rec_name = self.shop.price_list.rec_name
if not self.payment_term and self.invoice_address:
self.payment_term = self.shop.payment_term.id
self.payment_term.rec_name = self.shop.payment_term.rec_name
@classmethod
def get_paid_amount(cls, sales, names):
result = {n: {s.id: Decimal(0) for s in sales} for n in names}
for name in names:
for sale in sales:
for payment in sale.payments:
result[name][sale.id] += payment.amount
result[name][sale.id] += sale.get_total_vouchers_amount()
return result
def get_residual_amount(self, name=None):
total = self.total_amount_cache
if not total:
total = self.total_amount
return (total - self.paid_amount)
@classmethod
def set_number(cls, sales):
'''
Fill the number field with the sale shop or sale config sequence
'''
pool = Pool()
Config = pool.get('sale.configuration')
User = Pool().get('res.user')
config = Config(1)
user = User(Transaction().user)
for sale in sales:
if sale.number:
continue
elif sale.shop:
if sale.total_amount >= Decimal('0'):
number = sale.shop.sale_sequence.get()
else:
if not sale.shop.sale_return_sequence:
raise SaleMissingSequenceError(
gettext('sale_shop.msg_no_return_sequence'))
number = sale.shop.sale_return_sequence.get()
elif user.shop:
number = user.shop.sale_sequence.get()
else:
number = config.sale_sequence.get()
cls.write([sale], {
'number': number,
})
@classmethod
def create(cls, vlist):
vlist2 = []
config = Pool().get('sale.configuration')(1)
for vals in vlist:
User = Pool().get('res.user')
user = User(Transaction().user)
vals = vals.copy()
if 'shop' not in vals:
if not user.shop:
raise ShopUserError(
gettext('sale_shop.not_sale_shop', s=user.rec_name))
vals['shop'] = user.shop.id
vlist2.append(vals)
sales = super(Sale, cls).create(vlist2)
if config.on_draft_sequence:
for sale in sales:
if sale.invoice_type == '1' or sale.invoice_type == 'C':
cls.set_number([sale])
return sales
@classmethod
@ModelView.button_action('sale_shop.wizard_sale_payment')
def wizard_sale_payment(cls, sales):
pass
@classmethod
def write(cls, *args):
'''
Only edit Sale users available edit in this shop
'''
User = Pool().get('res.user')
user = User(Transaction().user)
if user.id != 0:
actions = iter(args)
for sales, _ in zip(actions, actions):
for sale in sales:
if not sale.shop:
raise SaleWriteError(
gettext('sale_shop.msg_sale_not_shop'))
elif sale.shop not in user.shops:
raise SaleWriteError(
gettext('sale_shop.msg_edit_sale_by_shop'))
super(Sale, cls).write(*args)
@classmethod
def delete(cls, sales):
for sale in sales:
if sale.number:
raise AccessError(
gettext('sale.msg_sale_delete_cancel',
sale=sale.rec_name))
super(Sale, cls).delete(sales)
def create_invoice(self):
invoice = super(Sale, self).create_invoice()
print('ingresa por este valor')
if not invoice:
return
if invoice.shop and invoice.shop.tax_exempt:
for line in invoice.lines:
if not line.product.account_category.account_other_income:
raise AccessError(
gettext('sale_shop.msg_dont_account_other_income',
product=line.product.rec_name))
line.account = line.product.account_category.account_other_income
return invoice
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
unit_price_w_tax = fields.Function(fields.Numeric('Unit Price with Tax',
digits=(16, Eval('_parent_sale', {}).get('currency_digits', 2)),
states={
'invisible': Eval('type') != 'line',
'readonly': Eval('sale_state') != 'draft',
}), 'on_change_with_unit_price_w_tax', setter='set_unit_price_w_tax')
amount_w_tax = fields.Function(fields.Numeric('Amount with Tax',
digits=(16, Eval('_parent_sale', {}).get('currency_digits', 2)),
states={
'invisible': Eval('type') != 'line',
'readonly': Eval('sale_state') != 'draft',
}), 'on_change_with_amount_w_tax', setter='set_amount_w_tax')
# TODO for remove this field
unit_price_full = fields.Numeric('Unit Price Full',
digits=(16, Eval('_parent_sale', {}).get('currency_digits',
Eval('currency_digits', 2))),
states={
'invisible': Eval('type') != 'line',
'readonly': True,
},
depends=['type', 'currency_digits'])
@staticmethod
def default_currency_digits():
Company = Pool().get('company.company')
if Transaction().context.get('company'):
company = Company(Transaction().context['company'])
return company.currency.digits
return 2
@staticmethod
def default_currency():
Company = Pool().get('company.company')
if Transaction().context.get('company'):
company = Company(Transaction().context['company'])
return company.currency.id
# @classmethod
# def get_price_w_tax(cls, lines, names):
# print('ingresa')
# pool = Pool()
# Tax = pool.get('account.tax')
# Company = pool.get('company.company')
# amount_w_tax = {}
# unit_price_w_tax = {}
# def compute_amount_with_tax(line):
# tax_amount = _ZERO
# amount = _ZERO
# if line.taxes:
# tax_list = Tax.compute(line.taxes, line.unit_price or _ZERO,
# line.quantity or 0.0)
# tax_amount = sum([t['amount'] for t in tax_list], _ZERO)
# if line.unit_price:
# amount = line.unit_price * Decimal(line.quantity)
# classification_tax = any(t.classification_tax == '02' for t in line.taxes)
# if hasattr(line.product, 'extra_tax') and line.product.extra_tax and classification_tax:
# amount += line.product.extra_tax * Decimal(line.quantity)
# return amount + tax_amount
# for line in lines:
# amount = _ZERO
# unit_price = _ZERO
# if line.sale:
# currency = line.sale.currency
# else:
# # company = Company(Transaction().context.get('company'))
# company = Company(1)
# currency = company.currency
# if line.type == 'line' and line.quantity:
# amount = compute_amount_with_tax(line)
# unit_price = amount / Decimal(str(line.quantity))
# # Only compute subtotals if the two fields are provided to speed up
# elif line.type == 'subtotal' and len(names) == 2:
# for line2 in line.sale.lines:
# if line2.type == 'line':
# amount2 = compute_amount_with_tax(line2)
# if currency:
# amount2 = currency.round(amount2)
# amount += amount2
# elif line2.type == 'subtotal':
# if line == line2:
# break
# amount = _ZERO
# if currency:
# amount = currency.round(amount)
# amount_w_tax[line.id] = amount
# unit_price_w_tax[line.id] = unit_price
# result = {
# 'amount_w_tax': amount_w_tax,
# 'unit_price_w_tax': unit_price_w_tax,
# }
# to_remove = []
# for key in list(result.keys()):
# if key not in names:
# to_remove.append(key)
# for rm in to_remove:
# del result[key]
# return result
@fields.depends('unit_price', 'quantity', 'taxes', 'product', 'currency')
def on_change_with_unit_price_w_tax(self, name=None):
if not self.quantity or self.unit_price is None:
return
currency_round = self.currency.round
if self.taxes:
pool = Pool()
Tax = pool.get('account.tax')
tax_list = Tax.compute(self.taxes, self.unit_price, 1)
tax_amount = sum([t['amount'] for t in tax_list], _ZERO)
classification_tax = any(t.classification_tax == '02' for t in self.taxes)
extra_tax = 0
if self.product.extra_tax and classification_tax:
extra_tax = self.product.extra_tax
return currency_round(tax_amount + extra_tax + self.unit_price)
else:
return currency_round(self.unit_price)
@fields.depends(
'quantity', 'unit_price_w_tax',
methods=['on_change_with_discount_rate', 'on_change_with_discount_amount', 'on_change_with_discount'])
def on_change_unit_price_w_tax(self):
if not self.unit_price_w_tax or not self.quantity:
return
currency_round = self.currency.round
if self.taxes:
pool = Pool()
Tax = pool.get('account.tax')
res = Tax.reverse_compute(self.unit_price_w_tax, self.taxes)
self.unit_price = round_price(res)
self.amount_w_tax = currency_round(self.unit_price_w_tax * Decimal(self.quantity))
else:
self.unit_price = self.unit_price_w_tax
self.amount_w_tax = currency_round(self.unit_price_w_tax * Decimal(self.quantity))
self.discount_rate = self.on_change_with_discount_rate()
self.discount_amount = self.on_change_with_discount_amount()
self.discount = self.on_change_with_discount()
@classmethod
def set_unit_price_w_tax(cls, lines, name, value):
pass
@fields.depends('unit_price_w_tax' , 'quantity', 'product', 'currency')
def on_change_with_amount_w_tax(self, name=None):
if self.unit_price_w_tax is None:
return
currency_round = self.currency.round
return currency_round(self.unit_price_w_tax * Decimal(self.quantity))
@fields.depends('amount_w_tax', 'quantity', 'taxes',
methods=['on_change_with_discount_rate', 'on_change_with_discount_amount', 'on_change_with_discount'])
def on_change_amount_w_tax(self):
pool = Pool()
Tax = pool.get('account.tax')
currency_round = self.currency.round
if self.taxes and self.amount_w_tax is not None and self.quantity:
self.unit_price_w_tax = currency_round(self.amount_w_tax / Decimal(self.quantity))
res = Tax.reverse_compute(self.unit_price_w_tax, self.taxes)
self.unit_price = round_price(res)
self.discount_rate = self.on_change_with_discount_rate()
self.discount_amount = self.on_change_with_discount_amount()
self.discount = self.on_change_with_discount()
@classmethod
def set_amount_w_tax(cls, lines, name, value):
pass
@fields.depends(
'product', 'unit', 'quantity', 'description',
'_parent_sale.party', '_parent_sale.currency', '_parent_sale.sale_date')
def on_change_product(self):
super(SaleLine, self).on_change_product()
if self.product:
desc = self.product.template.name
if self.product.description:
desc += ' | ' + self.product.description
self.description = desc
# @fields.depends('product', 'quantity', 'unit', 'taxes',
# '_parent_sale.currency', '_parent_sale.party',
# '_parent_sale.sale_date', 'unit_price_full')
# def on_change_quantity(self):
# super(SaleLine, self).on_change_quantity()
# if self.unit_price_full:
# self.on_change_unit_price_full()
class SaleBySupplierStart(ModelView):
'Sale By Supplier Start'
__name__ = 'sale_shop.sale_by_supplier.start'
start_date = fields.Date('Start Date', required=True)
end_date = fields.Date('End Date', required=True)
suppliers = fields.Many2Many('party.party', None, None, 'Suppliers')
shop = fields.Many2One('sale.shop', 'Shop')
company = fields.Many2One('company.company', 'Company', required=True)
detailed = fields.Boolean('Detailed')
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_end_date():
Date_ = Pool().get('ir.date')
return Date_.today()
class PrintSaleBySupplier(Wizard):
'Sale By Supplier'
__name__ = 'sale_shop.print_sale_by_supplier'
start = StateView(
'sale_shop.sale_by_supplier.start',
'sale_shop.print_sale_by_supplier_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('sale_shop.sale_by_supplier_report')
def do_print_(self, action):
suppliers = [s.id for s in self.start.suppliers]
shop_id = None
shop_name = ''
if self.start.shop:
shop_id = self.start.shop.id
shop_name = self.start.shop.name
data = {
'ids': [],
'company': self.start.company.id,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'suppliers': suppliers,
'detailed': self.start.detailed,
'shop': shop_id,
'shop_name': shop_name,
}
return action, data
def transition_print_(self):
return 'end'
class SaleBySupplier(Report):
'Sale By Supplier Report'
__name__ = 'sale_shop.sale_by_supplier_report'
@classmethod
def get_last_purchase(cls, product_id):
PurchaseLine = Pool().get('purchase.line')
lines = PurchaseLine.search([
('product', '=', product_id),
('purchase.state', 'in', ('processing', 'done')),
], order=[('create_date', 'DESC')], limit=1)
if lines:
line = lines[0]
return (line.purchase.purchase_date, line.quantity)
else:
return None, None
@classmethod
def get_margin(cls, pline):
res = 0
total_cost = pline['cost_price'] * pline['quantity']
if pline['amount'] != 0:
res = 1 - (total_cost / pline['amount'])
return res
@classmethod
def get_last_sales(cls, product_id, end_date, days):
InvoiceLine = Pool().get('account.invoice.line')
start_date = end_date - timedelta(days=days)
lines = InvoiceLine.search([
('product', '=', product_id),
('invoice.invoice_date', '>=', start_date),
('invoice.invoice_date', '<=', end_date),
('invoice.state', 'in', ('posted', 'paid')),
('invoice.type', '=', 'out'),
('type', '=', 'line'),
])
return sum([l.quantity for l in lines]), start_date
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
Party = pool.get('party.party')
ProductSupplier = pool.get('purchase.product_supplier')
company = Company(data['company'])
cursor = Transaction().connection.cursor()
dom_suppliers = [
('template', '!=', None)
]
total_sales = []
total_sales_append = total_sales.append
parties_query = ''
start_date = data['start_date']
end_date = data['end_date']
delta = (data['end_date'] - data['start_date']).days + 1
if data['suppliers']:
dom_suppliers.append([
('party', 'in', data['suppliers']),
])
if len(data['suppliers']) == 1:
supplier = data['suppliers'][0]
parties_query = f' AND pu.party={supplier}'
else:
parties = str(tuple(data['suppliers']))
parties_query = f' AND pu.party IN {parties}'
products_suppliers = ProductSupplier.search(dom_suppliers)
products_dict = {}
for ps in products_suppliers:
if ps.template.products:
product_id = ps.template.products[0].id
products_dict[product_id] = ps.party.id
products_query = ''
if data['suppliers']:
keys = products_dict.keys()
if len(keys) == 1:
product_id = keys[0]
products_query = f' AND al.product={product_id} '
elif keys:
products_ids = str(tuple(keys))
products_query = f' AND al.product IN {products_ids} '
shops_query = ''
start_date = data['start_date']
if data['shop']:
shop = data['shop']
shops_query = f' AND ai.shop={shop} '
query = f"""
SELECT
al.product,
pp.code,
pt.name,
SUM(al.unit_price * al.quantity) AS amount,
SUM(pc.cost_price * al.quantity) AS cost,
SUM(al.quantity) AS quantity
FROM account_invoice_line AS al
JOIN account_invoice AS ai ON al.invoice=ai.id
JOIN product_product AS pp ON al.product=pp.id
JOIN product_template AS pt ON pp.template=pt.id
JOIN product_cost_price AS pc ON pc.product=pp.id
WHERE ai.invoice_date>='{start_date}' AND ai.invoice_date<='{end_date}'
AND ai.state IN ('posted', 'paid')
AND ai.type='out' AND al.type='line'
{products_query}
{shops_query}
GROUP BY al.product, pp.code, pt.name
"""
cursor.execute(query)
_records = cursor.fetchall()
def compute_margin(sales, costs):
margin = 0
if sales and sales != 0:
margin = 1 - costs / sales
return margin
# suppliers = {0: {
# 'name': 'NA',
# 'products': [],
# 'sales': [],
# 'costs': [],
# }}
suppliers = {}
def _rec2dict(rec):
return {
'code': rec[1],
'name': rec[2],
'amount': rec[3],
'quantity': rec[5],
'margin': compute_margin(rec[3], rec[4]),
'daily_sale': rec[5] / delta,
}
for rec in _records:
total_sales_append(rec[3])
pd_id = rec[0]
party_id = products_dict.get(pd_id, 0)
try:
if data['detailed']:
suppliers[party_id]['products'].append(_rec2dict(rec))
suppliers[party_id]['sales'].append(rec[3])
suppliers[party_id]['costs'].append(rec[4])
except Exception:
parties = Party.search_read([
('id', '=', int(party_id))
], fields_names=['name'])
if not parties:
continue
party = parties[0]
party_id = party['id']
suppliers[party_id] = {
'name': party['name'],
'sales': [rec[3]],
'costs': [rec[4]]
}
if data['detailed']:
suppliers[party_id]['products'] = [_rec2dict(rec)]
for key, value in suppliers.items():
suppliers[key]['sales'] = sum(value['sales'])
suppliers[key]['costs'] = sum(value['costs'])
# Maybe we need this in the future
# sales_30, date_ = cls.get_last_sales(pline['product_id'],
# data['start_date'], 30)
# sales_60, _ = cls.get_last_sales(pline['product_id'],
# date_, 60)
records = suppliers.values()
report_context['records'] = records
report_context['start_date'] = data['start_date']
report_context['end_date'] = data['end_date']
report_context['today'] = date.today()
report_context['company'] = company.party.name
report_context['shop'] = data['shop_name']
report_context['total_sales'] = sum(total_sales)
report_context['detailed'] = data['detailed']
report_context['compute_margin'] = compute_margin
return report_context
class SaleShopDetailedStart(ModelView):
'Sale Shop Detailed Start'
__name__ = 'sale_shop.sale_detailed.start'
start_date = fields.Date('Start Date', required=True)
end_date = fields.Date('End Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
salesman = fields.Many2One('company.employee', 'Salesman')
party = fields.Many2One('party.party', 'Party')
product = fields.Many2One('product.product', 'Product')
shop = fields.Many2One('sale.shop', 'Shop')
categories = fields.Many2Many('product.category', None, None, 'Categories')
# groupby_category = fields.Boolean('Group By Category')
# groupby_products = fields.Boolean('Group By Products')
@staticmethod
def default_company():
return Transaction().context.get('company')
class SaleShopDetailed(Wizard):
'Sale Shop Detailed'
__name__ = 'sale_shop.sale_detailed'
start = StateView(
'sale_shop.sale_detailed.start',
'sale_shop.sale_shop_detailed_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateAction('sale_shop.report_sale_detailed')
def do_print_(self, action):
category_ids = []
salesman_id = None
party_id = None
product_id = None
shop_id = None
if self.start.salesman:
salesman_id = self.start.salesman.id
if self.start.shop:
shop_id = self.start.shop.id
if self.start.party:
party_id = self.start.party.id
if self.start.product:
product_id = self.start.product.id
if self.start.categories:
category_ids = [acc.id for acc in self.start.categories]
# if self.start.categories:
# data['categories'] = [c.id for c in self.start.categories]
data = {
'company': self.start.company.id,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'salesman': salesman_id,
'party': party_id,
'product': product_id,
'categories': category_ids,
'shop': shop_id,
}
return action, data
def transition_print_(self):
return 'end'
class SaleShopDetailedReport(Report):
'Sale Shop Detailed Report'
__name__ = 'sale_shop.report_sale_detailed'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
Company = pool.get('company.company')
Category = pool.get('product.template-product.category')
categories = Category.search([('category', 'in', data['categories'])])
products_ids = [c.template.id for c in categories]
invoice_line_dom = [
('invoice.invoice_date', '>=', data['start_date']),
('invoice.invoice_date', '<=', data['end_date']),
('invoice.company', '=', data['company']),
('invoice.type', '=', 'out'),
('invoice.state', 'in', ['posted', 'paid', 'validated']),
]
if data['salesman']:
invoice_line_dom.append(
('invoice.salesman', '=', data['salesman'])
)
if data['shop']:
invoice_line_dom.append(
('invoice.shop', '=', data['shop'])
)
if data['party']:
invoice_line_dom.append(
('invoice.party', '=', data['party'])
)
if data['product']:
invoice_line_dom.append(
('product', '=', data['product'])
)
if data['categories']:
if products_ids:
invoice_line_dom.append(
('product.template', 'in', products_ids)
)
else:
invoice_line_dom.append(
('product.template.account_category', 'in', data['categories'])
)
# if data['categories']:
# invoice_line_dom.append(
# ('product.categories', 'in', data['categories'])
# )
config = pool.get('sale.configuration')(1)
products_exception = []
if hasattr(config, 'tip_product') and config.tip_product and config.exclude_tip_and_delivery:
products_exception.append(config.tip_product.id)
if hasattr(config, 'delivery_product') and config.delivery_product and config.exclude_tip_and_delivery:
products_exception.append(config.delivery_product.id)
if len(products_exception) > 0:
invoice_line_dom.append(('product', 'not in', products_exception))
start_lines = InvoiceLine.search(
invoice_line_dom, order=[('invoice.invoice_date', 'ASC')]
)
lines = []
amount = []
# amount_w_tax = []
for line in start_lines:
if line.type == 'line' and not line.product:
raise SaleValidationError(
gettext('sale_shop.msg_sale_not_product', s=line.sale.number
))
amount.append(line.amount)
lines.append(line)
# is_invoiced = False
# if line.invoice:
# for inv in line.sale.invoices:
# if line.invoice.state not in ['draft', 'cancel']:
# is_invoiced = True
# if is_invoiced:
# lines.append(line)
# # amount_w_tax.append(line.amount_w_tax)
# else:
# amount.append(line.amount)
# # amount_w_tax.append(line.amount_w_tax)
# report_context['amount_w_tax'] = sum(amount_w_tax)
report_context['amount'] = sum(amount)
report_context['records'] = lines
report_context['company'] = Company(data['company'])
return report_context
class SaleBySellerStart(ModelView):
'Sale By Seller Start'
__name__ = 'sale_shop.sale_by_seller.start'
start_date = fields.Date('Start Date', required=True)
end_date = fields.Date('End Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
shop = fields.Many2One('sale.shop', 'Shop', depends=['company'],
domain=[
('company', '=', Eval('company'))
])
salesmans = fields.Many2Many(
'company.employee', None, None, 'Salesmans')
payment_methods = fields.Many2Many(
'account.invoice.payment_term', None, None, 'Payment Method')
@staticmethod
def default_company():
return Transaction().context.get('company')
class SaleBySeller(Wizard):
'Sale By Seller'
__name__ = 'sale_shop.sale_by_seller.wizard'
start = StateView(
'sale_shop.sale_by_seller.start',
'sale_shop.sale_by_seller_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateAction('sale_shop.sale_by_seller_report_')
def do_print_(self, action):
data = {
'ids': [],
'company': self.start.company.id,
'company_name': self.start.company.rec_name,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'shop': None,
'shop_name': None,
'payment_terms': None,
'salesmans': None,
}
shop = self.start.shop
if shop:
data['shop'] = self.start.shop.id
data['shop_name'] = self.start.shop.rec_name
if self.start.payment_methods:
data['payment_terms'] = [s.id for s in self.start.payment_methods]
if self.start.salesmans:
data['salesmans'] = [s.id for s in self.start.salesmans]
return action, data
def transition_print_(self):
return 'end'
class SaleBySellerReport(Report):
__name__ = 'sale_shop.sale_by_seller_report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Invoice = pool.get('account.invoice')
Company = pool.get('company.company')
shop = data['shop']
invoice_dom = [
('invoice_date', '>=', data['start_date']),
('invoice_date', '<=', data['end_date']),
('company', '=', data['company']),
('state', 'in', ['posted', 'paid']),
('type', '=', 'out'),
]
if shop:
invoice_dom.append(
('shop', '=', shop),
)
if data['payment_terms']:
invoice_dom.append(('payment_term', 'in', data['payment_terms']))
if data['salesmans']:
invoice_dom.append(('salesman', 'in', data['salesmans']))
invoices = Invoice.search(invoice_dom)
lines = {}
sum_total_amount = 0
sum_total_cash = 0
sum_total_credit = 0
for line in invoices:
seller = line.salesman
if seller:
cash = 0
credit = 0
seller_id = line.salesman.id
payment_term_type = line.payment_term.payment_type
amount = line.untaxed_amount
if payment_term_type == '1':
cash = amount
else:
credit = amount
if seller_id not in lines.keys():
lines[seller_id] = {
'shop': line.shop.rec_name,
'seller': line.salesman.rec_name,
'cash': cash,
'credit': credit,
'amount': amount,
}
sum_total_amount += amount
sum_total_cash += cash
sum_total_credit += credit
else:
lines[seller_id]['cash'] += cash
lines[seller_id]['credit'] += credit
lines[seller_id]['amount'] += amount
sum_total_amount += amount
sum_total_cash += cash
sum_total_credit += credit
report_context['records'] = lines.values()
report_context['sum_total_amount'] = sum_total_amount
report_context['sum_total_cash'] = sum_total_cash
report_context['sum_total_credit'] = sum_total_credit
report_context['company'] = Company(data['company'])
report_context['company_name'] = data['company_name']
return report_context
class SaleMonthByShopStart(ModelView):
'Sale Month By Shop Start'
__name__ = 'sale_shop.sale_month_shop.start'
company = fields.Many2One('company.company', 'Company', required=True)
shop = fields.Many2One('sale.shop', 'Shop', required=True,
depends=['company'], domain=[
('company', '=', Eval('company'))
])
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year',
required=True)
period = fields.Many2One('account.period', 'Period',
depends=['fiscalyear'], required=True, domain=[
('fiscalyear', '=', Eval('fiscalyear')),
])
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_fiscalyear():
FiscalYear = Pool().get('account.fiscalyear')
return FiscalYear.find(
Transaction().context.get('company'), exception=False)
@fields.depends('fiscalyear')
def on_change_fiscalyear(self):
self.period = None
class SaleMonthByShop(Wizard):
'Sale MonthByShop'
__name__ = 'sale_shop.sale_month_shop'
start = StateView(
'sale_shop.sale_month_shop.start',
'sale_shop.sale_month_shop_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('sale_shop.sale_month_shop_report')
def do_print_(self, action):
data = {
'company': self.start.company.id,
'shop': self.start.shop.id,
'period': self.start.period.id,
'fiscalyear': self.start.fiscalyear.id,
}
return action, data
def transition_print_(self):
return 'end'
class SaleMonthByShopReport(Report):
'Sale Month By Shop'
__name__ = 'sale_shop.sale_month_shop_report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
company = Company(data['company'])
Invoice = pool.get('account.invoice')
Line = pool.get('account.invoice.line')
Payment_term = pool.get('account.invoice.payment_term')
Period = pool.get('account.period')
Shop = pool.get('sale.shop')
period = Period(data['period'])
config = pool.get('sale.configuration')(1)
products_exception = []
if hasattr(config, 'tip_product') and config.tip_product and config.exclude_tip_and_delivery:
products_exception.append(config.tip_product.id)
if hasattr(config, 'delivery_product') and config.delivery_product and config.exclude_tip_and_delivery:
products_exception.append(config.delivery_product.id)
totals = {}
if len(products_exception) > 0:
invoice = Invoice.__table__()
line = Line.__table__()
payment_term = Payment_term.__table__()
where = invoice.state.in_(['posted', 'paid', 'validated'])
where &= invoice.invoice_date >= period.start_date
where &= In(line.product, products_exception)
where &= invoice.invoice_date <= period.end_date
where &= invoice.shop == data['shop']
where &= invoice.company == data['company']
where &= invoice.type == 'out'
cursor = Transaction().connection.cursor()
cursor.execute(*line.join(
invoice, condition=line.invoice == invoice.id).join(
payment_term, condition=invoice.payment_term == payment_term.id).select(
Extract('DAY', invoice.invoice_date).as_('Day'),
Sum(Case((payment_term.payment_type == '1', line.quantity*line.unit_price), else_=0)).as_('amount_cash'),
Sum(Case((payment_term.payment_type != '1', line.quantity*line.unit_price), else_=0)).as_('amount_credit'),
Sum(line.quantity*line.unit_price).as_('amount'),
where=where,
group_by=(Extract('DAY', invoice.invoice_date))
))
result = cursor.fetchall()
columns = list(cursor.description)
for row in result:
row_dict = {}
for i, col in enumerate(columns):
row_dict[col.name] = row[i]
totals[int(row[0])] = row_dict
invoices = Invoice.search([
('company', '=', data['company']),
('shop', '=', data['shop']),
('invoice_date', '>=', period.start_date),
('invoice_date', '<=', period.end_date),
('type', '=', 'out'),
('state', 'in', ['posted', 'paid', 'validated']),
], order=[('invoice_date', 'ASC')])
year = period.start_date.year
month = period.start_date.month
_, last_day = calendar.monthrange(year, period.start_date.month)
mdays = {(nday + 1): {
'date': date(year, month, nday + 1),
'num_invoices': [],
'untaxed_amount': [],
'tax_amount': [],
'total_amount': [],
'cash': [],
'credit': []
} for nday in range(last_day)}
sum_untaxed_amount = []
sum_total_cash = []
sum_total_credit = []
sum_tax_amount = []
sum_total_amount = []
def _is_credit(payment_term):
res = []
for line in payment_term.lines:
res.append(sum([d.months + d.weeks + d.days
for d in line.relativedeltas]))
if sum(res) > 0:
return True
return False
for invoice in invoices:
if _is_credit(invoice.payment_term):
mdays[invoice.invoice_date.day]['credit'].append(invoice.total_amount)
else:
mdays[invoice.invoice_date.day]['cash'].append(invoice.total_amount)
mdays[invoice.invoice_date.day]['num_invoices'].append(1)
mdays[invoice.invoice_date.day]['untaxed_amount'].append(invoice.untaxed_amount)
mdays[invoice.invoice_date.day]['tax_amount'].append(invoice.tax_amount)
mdays[invoice.invoice_date.day]['total_amount'].append(invoice.total_amount)
for k, v in mdays.items():
if k in totals.keys():
mdays.update({k: {
'date': mdays[k]['date'],
'num_invoices': sum(mdays[k]['num_invoices']),
'untaxed_amount': sum(mdays[k]['untaxed_amount']) - Decimal(totals[k]['amount']),
'tax_amount': sum(mdays[k]['tax_amount']),
'total_amount': sum(mdays[k]['total_amount']) - Decimal(totals[k]['amount']),
'cash': sum(mdays[k]['cash']) - Decimal(totals[k]['amount_cash']),
'credit': sum(mdays[k]['credit']) - Decimal(totals[k]['amount_credit'])
}
})
else:
mdays.update({k: {
'date': mdays[k]['date'],
'num_invoices': sum(mdays[k]['num_invoices']),
'untaxed_amount': sum(mdays[k]['untaxed_amount']),
'tax_amount': sum(mdays[k]['tax_amount']),
'total_amount': sum(mdays[k]['total_amount']),
'cash': sum(mdays[k]['cash']),
'credit': sum(mdays[k]['credit'])
}
})
sum_total_cash.append(mdays[k]['cash'])
sum_total_credit.append(mdays[k]['credit'])
sum_untaxed_amount.append(mdays[k]['untaxed_amount'])
sum_tax_amount.append(mdays[k]['tax_amount'])
sum_total_amount.append(mdays[k]['total_amount'])
report_context['records'] = mdays.values()
report_context['total_credit'] = sum(sum_total_credit)
report_context['total_cash'] = sum(sum_total_cash)
report_context['untaxed_amount'] = sum(sum_untaxed_amount)
report_context['tax_amount'] = sum(sum_tax_amount)
report_context['total_amount'] = sum(sum_total_amount)
report_context['shop'] = Shop(data['shop']).name
report_context['period'] = period.name
report_context['company'] = company.party.name
return report_context
class SalePaymentForm(ModelView):
'Sale Payment Form'
__name__ = 'sale.payment.form'
statement = fields.Many2One('account.statement', 'Statement',
required=True)
amount_payable = fields.Numeric('Amount Payable', required=True,
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
currency_digits = fields.Integer('Currency Digits')
party = fields.Many2One('party.party', 'Party', readonly=True)
require_voucher = fields.Boolean('Require Voucher',
depends=['statement'])
voucher = fields.Char('Voucher Number', states={
'required': Eval('require_voucher', False),
'invisible': Not(Eval('require_voucher', False)),
}, depends=['require_voucher'])
self_pick_up = fields.Boolean('Self Pick Up', readonly=True)
do_invoice = fields.Boolean('Do Invoice')
@classmethod
def __setup__(cls):
super(SalePaymentForm, cls).__setup__()
@fields.depends('statement', 'voucher')
def on_change_with_require_voucher(self):
if self.statement:
return self.statement.journal.require_voucher
else:
return False
@classmethod
def default_require_voucher(cls):
return False
@classmethod
def default_do_invoice(cls):
return True
class WizardSalePayment(Wizard):
'Wizard Sale Payment'
__name__ = 'sale.payment'
start = StateView(
'sale.payment.form',
'sale_shop.sale_payment_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Pay', 'pay_', 'tryton-ok', default=True),
])
pay_ = StateTransition()
@classmethod
def __setup__(cls):
super(WizardSalePayment, cls).__setup__()
@classmethod
def execute(cls, session_id, data, state_name):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale(Transaction().context['active_id'])
result = super().execute(session_id, data, state_name)
dom_statement = [
('state', '=', 'draft')
]
context = Transaction().context
if context.get('sale_device', None) and sale.sale_device:
dom_statement.append(('sale_device', '=', sale.sale_device.id))
if result.get('view'):
result['view']['fields_view']['fields']['statement']['domain'] = dom_statement
return result
def default_start(self, fields):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale(Transaction().context['active_id'])
return {
'amount_payable': abs(sale.total_amount - sale.paid_amount
if sale.paid_amount else sale.total_amount),
'currency_digits': sale.currency_digits,
'party': sale.party.id,
}
def transition_pay_(self):
pool = Pool()
Sale = pool.get('sale.sale')
StatementLine = pool.get('account.statement.line')
active_id = Transaction().context.get('active_id', False)
sale = Sale(active_id)
form = self.start
if form.amount_payable > 0:
if not sale.number:
Sale.set_number([sale])
if not sale.party.account_receivable:
raise PartyMissingAccount(
gettext('sale_shop.msg_party_without_account_receivable', s=sale.party.name))
account = sale.party.account_receivable.id
if form.amount_payable:
amount = form.amount_payable
if sale.total_amount < 0:
amount = amount * -1
st_line = StatementLine(
statement=form.statement.id,
date=date.today(),
amount=amount,
party=sale.party.id,
account=account,
description=self.start.voucher,
sale=active_id,
number=sale.number,
)
st_line.save()
st_line.create_move()
if sale.total_amount != sale.paid_amount:
return 'start'
sale.save()
# Overload this method in sale pos
# if self.start.do_invoice:
# # for inv in sale.invoices:
# # if inv.state == 'posted':
# # inv.write([inv], {'state': 'draft'})
# Sale.workflow_to_end([sale])
return 'end'
class MultiplePaymentSaleStart(ModelView):
'Multiple Payment Sale Start'
__name__ = 'multiple_payment_sale.start'
statement = fields.Many2One('account.statement', 'Statement',
required=True, domain=[('state', '=', 'draft')])
party = fields.Many2One('party.party', 'Party', required=True)
amount = fields.Numeric('Amount', digits=(16, 2))
sales = fields.One2Many('select_multiple_payment_sale', 'line', 'Sales',
context={'party': Eval('party')})
balance = fields.Numeric('Balance', digits=(16, 2), readonly=True)
id = fields.Integer('Id', readonly=True)
company = fields.Many2One('company.company', 'Company', readonly=True)
description = fields.Char('Description')
@staticmethod
def default_id():
return 1
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_amount():
return Decimal(0)
@staticmethod
def default_balance():
return Decimal(0)
@fields.depends('sales', 'amount')
def on_change_sales(self):
if self.amount:
self.balance = self.amount - sum(s.amount_to_pay for s in self.sales if s.amount_to_pay)
@fields.depends('sales', 'amount')
def on_change_amount(self):
if self.amount and self.sales:
self.balance = self.amount - sum(s.amount_to_pay for s in self.sales if s.amount_to_pay)
class SelectMultiplePaymentSale(ModelView):
'Select Multiple Payment Sale'
__name__ = 'select_multiple_payment_sale'
line = fields.Many2One('multiple_payment_sale.start', 'Line Id')
sale = fields.Many2One('sale.sale', 'Sale', domain=[
('state', 'in', ['confirmed', 'processing']),
('party', '=', Eval('party')),
], depends=['party'])
residual_amount = fields.Numeric('residual amount', digits=(16, 2),
readonly=True)
amount_to_pay =fields.Numeric('amount to pay', digits=(16, 2),
help="Amount to pay must be less than balance")
party = fields.Many2One('party.party', 'Party')
company = fields.Many2One('company.company', 'Company', readonly=True)
@staticmethod
def default_line():
return 1
@fields.depends(
'line', '_parent_line.party', '_parent_line.company')
def on_change_line(self):
if self.line:
self.party = self.line.party
self.company = self.line.company
@fields.depends('residual_amount', 'amount_to_pay',
'sale', 'line', '_parent_line.balance')
def on_change_sale(self):
if self.sale:
residual = self.sale.residual_amount
self.residual_amount = residual
self.amount_to_pay = residual
else:
self.residual_amount = None
self.amount_to_pay = None
@fields.depends('amount_to_pay', 'line', 'residual_amount')
def on_change_amount_to_pay(self):
if self.amount_to_pay > self.line.balance or self.amount_to_pay > self.residual_amount:
self.amount_to_pay = None
class MultiplePaymentSale(Wizard):
'Multiple Payment Sale'
__name__ = 'multiple_payment_sale'
start = StateView(
'multiple_payment_sale.start',
'sale_shop.multiple_payment_sale_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create Moves', 'create_moves', 'tryton-ok', default=True),
Button('Select Sales', 'select_sales_ask'),
])
select_sales_ask = StateView(
'sale.shop.select_sales.ask',
'sale_shop.select_sales_ask_view_form', [
Button('Add', 'add_sales', 'tryton-ok', default=True),
])
create_moves = StateTransition()
add_sales = StateTransition()
def default_select_sales_ask(self, fields):
return {
'party': self.start.party.id,
'statement': self.start.statement.id
}
def default_start(self, fields):
default = {}
if hasattr(self.select_sales_ask, 'party'):
default.update({'party': self.select_sales_ask.party.id})
if hasattr(self.select_sales_ask, 'statement'):
default.update({'statement': self.select_sales_ask.statement.id})
if hasattr(self.select_sales_ask, 'sales'):
default.update(
{'sales': [
{'sale': s.id, 'party': self.select_sales_ask.party.id} for s in self.select_sales_ask.sales
]})
return default
@classmethod
def execute(cls, session_id, data, state_name):
result = super().execute(session_id, data, state_name)
if result.get('view') and state_name == 'select_sales_ask':
sale_domain = [
('state', 'in', ['confirmed', 'processing']),
('invoice_state', '!=', 'paid'),
]
if result['view']['defaults'].get('party'):
sale_domain.append(('party', '=', result['view']['defaults']['party']))
result['view']['fields_view']['fields']['sales']['domain']= sale_domain
return result
def transition_create_moves(self):
pool = Pool()
StatementLine = pool.get('account.statement.line')
statement = self.start.statement
description = self.start.description
party = self.start.party
account = party.account_receivable
for line in self.start.sales:
st_line = StatementLine(
statement=statement,
amount=line.amount_to_pay,
party=party,
sale=line.sale,
account=account,
description=description,
date=date.today(),
number=line.sale.number,
)
st_line.save()
return 'end'
def transition_add_sales(self):
return 'start'
class SelectSalesAsk(ModelView):
'Select Sales Ask'
__name__ = 'sale.shop.select_sales.ask'
sales = fields.Many2Many('sale.sale', None, None, 'Sales')
party = fields.Many2One('party.party', 'Party', readonly=True,
states={'invisible': True})
statement = fields.Many2One('account.statement', 'Statement',
readonly=True, states={'invisible': True})