trytonpsk-farming/sale.py

708 lines
25 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.
from datetime import timedelta, date
from itertools import chain
from sql import Table
from decimal import Decimal
from trytond.model import fields, ModelSQL, ModelView
from trytond.wizard import (
Wizard, StateView, Button, StateTransition, StateReport)
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.report import Report
STATES = {
'readonly': Eval('state') == 'done'
}
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
carrier = fields.Many2One('party.party', 'Carrier', states=STATES)
freight_forwader = fields.Many2One('party.party', 'Freight Forwader',
states=STATES, help="The agent")
mawb = fields.Char('MAWB', states=STATES)
hawb = fields.Char('HAWB', states=STATES)
charge = fields.Many2One('exportation.charge', 'Charge', states={
'readonly': False,
})
export_target_city = fields.Many2One('party.city_code',
'Export Target City', required=True, select=True)
export_route = fields.Char('Export Route', select=True)
boxes = fields.Function(fields.Float('Boxes', digits=(6, 2)),
'get_lines_totals')
unit_qty = fields.Function(fields.Float('Unit Qty', digits=(6, 0)),
'get_lines_totals')
packing_qty = fields.Function(fields.Float('Packing Qty', digits=(6, 0)),
'get_lines_totals')
quantity = fields.Function(fields.Float('Quantity', digits=(6, 0)),
'get_lines_totals')
aged = fields.Function(fields.Integer('Aged'), 'get_aged')
total_amount_ref = fields.Function(fields.Numeric('Total Amount Ref.',
digits=(6, 2)), 'get_total_amount_ref')
custom_global = fields.Many2One('party.customs_global', 'Global',
domain=[('party', '=', Eval('party'))])
@classmethod
def __setup__(cls):
super(Sale, cls).__setup__()
cls.shipping_date.states = {
'readonly': Eval('state') == 'done',
}
cls.lines.states = {
'readonly': Eval('state') == 'done',
}
@fields.depends('party', 'invoice_party', 'shipment_party',
'payment_term', 'currency', 'carrier', 'freight_forwader')
def on_change_party(self):
super(Sale, self).on_change_party()
if self.party:
if self.party.currency:
self.currency = self.party.currency.id
if self.party.carrier:
self.carrier = self.party.carrier.id
if self.party.freight_forwader:
self.freight_forwader = self.party.freight_forwader.id
if self.party.addresses:
city_code = self.party.addresses[0].city_code
self.export_target_city = city_code.id if city_code else None
if self.party.export_route:
self.export_route = self.party.export_route
@classmethod
def _get_untaxed_amount(cls, sale):
return sum(
(line.amount for line in sale.lines
if line.type == 'line'), Decimal(0)
)
def get_total_amount_ref(self, name=None):
res = []
for line in self.lines:
if line.unit_price_ref:
res.append(Decimal(line.quantity) * line.unit_price_ref)
return sum(res)
@classmethod
def process(cls, sales):
sale = sales[0]
sale.untaxed_amount_cache = cls._get_untaxed_amount(sale)
sale.tax_amount_cache = sale.get_tax_amount()
sale.total_amount_cache = sale.tax_amount_cache + sale.untaxed_amount_cache
sale.save()
super(Sale, cls).process(sales)
for sale in sales:
for line in sale.lines:
if not line.production and line.product.template.producible:
production_id = cls.create_production(line)
if production_id:
line.production = production_id
line.save()
@classmethod
def copy(cls, sales, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('shipping_date', None)
return super(Sale, cls).copy(sales, default)
@classmethod
def create_production(cls, line):
pool = Pool()
Production = pool.get('production')
sale = line.sale
planned_date = sale.shipping_date - timedelta(days=1)
if planned_date < sale.sale_date:
planned_date = sale.sale_date
data = {
'reference': sale.reference or sale.number,
'customer': sale.party.id,
'delivery_date': sale.shipping_date,
'planned_date': sale.shipping_date,
'planned_start_date': sale.shipping_date,
'effective_date': sale.shipping_date,
'effective_start_date': sale.shipping_date,
'company': sale.company.id,
'warehouse': sale.warehouse.id,
'location': sale.warehouse.production_location.id,
'product': line.product.id,
'uom': line.packing_uom.id,
'quantity': line.packing_qty,
'state': 'request',
'primary': True,
'origin': str(line),
'factor_packing': line.factor_packing,
'outputs': [('create', [{
'product': line.product.id,
'quantity': line.packing_qty,
'uom': line.packing_uom.id,
'unit_price': line.product.cost_price,
'from_location': sale.warehouse.production_location.id,
'to_location': sale.warehouse.storage_location.id,
}])],
}
_inputs = []
subproductions = []
for kit in line.kits:
input = {
'product': kit.product.id,
'quantity': kit.quantity * line.packing_qty,
'uom': kit.product.default_uom.id,
'from_location': sale.warehouse.storage_location.id,
'to_location': sale.warehouse.production_location.id,
'style': kit.style.id if kit.style else None,
}
if kit.product.producible:
_production = cls._get_production(kit, sale, line)
input['production'] = _production
subproductions.append(_production)
_inputs.append(input)
data['inputs'] = [('create', _inputs)]
data['subproductions'] = [('add', subproductions)]
if data:
data['state'] = 'waiting'
productions = Production.create([data])
if productions:
return productions[0].id
@classmethod
def _get_production(cls, kit, sale, line):
pool = Pool()
Production = pool.get('production')
data = {
'reference': sale.number + ' | ' + line.summary,
'company': sale.company.id,
'customer': sale.party.id,
'warehouse': sale.warehouse.id,
'delivery_date': sale.shipping_date,
'location': sale.warehouse.production_location.id,
'planned_date': sale.shipping_date,
'planned_start_date': sale.shipping_date,
'effective_date': sale.shipping_date,
'effective_start_date': sale.shipping_date,
'product': kit.product.id,
'uom': kit.product.default_uom.id,
'quantity': kit.quantity * line.packing_qty,
'state': 'waiting',
'origin': str(line),
'outputs': [('create', [{
'product': kit.product.id,
'quantity': kit.quantity * line.packing_qty,
'uom': kit.uom.id,
'unit_price': kit.product.cost_price,
'from_location': sale.warehouse.production_location.id,
'to_location': sale.warehouse.storage_location.id,
}])],
}
inputs = []
for com in kit.components:
inputs.append({
'product': com.product.id,
'quantity': com.quantity * kit.quantity * line.packing_qty * com.product.default_uom.rate,
'uom': com.product.default_uom.id,
'from_location': sale.warehouse.storage_location.id,
'to_location': sale.warehouse.production_location.id,
'style': com.style.id if com.style else None,
})
data['inputs'] = [('create', inputs)]
data['state'] = 'waiting'
pruduction, = Production.create([data])
return pruduction.id
def get_aged(self, name=None):
if self.sale_date:
aged = (date.today() - self.shipping_date).days
return aged
def get_lines_totals(self, name=None):
return sum(getattr(line, name) or 0 for line in self.lines)
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
factor_packing = fields.Float('Factor Packing')
reference = fields.Function(fields.Char('Reference'), 'get_reference')
customer = fields.Function(fields.Many2One('party.party',
'Customer'), 'get_customer')
boxes = fields.Function(fields.Float('Boxes', digits=(6, 2)),
'on_change_with_boxes')
unit_qty = fields.Function(fields.Float('Unit Qty', digits=(10, 0)),
'on_change_with_unit_qty')
packing_uom = fields.Many2One('product.uom', 'Packing UoM')
packing_qty = fields.Float('Packing Qty')
kits = fields.One2Many('sale.line.kit', 'line', 'Line Kit')
productions = fields.One2Many('production', 'origin', 'Productions')
charge_line = fields.Many2One('exportation.charge.line', 'Charge Line')
production = fields.Many2One('production', 'Production')
quality_analysis = fields.One2Many('farming.quality.analysis', 'origin',
'Quality Analysis', states=STATES)
unit_price_ref = fields.Numeric('Price Ref.', digits=(6, 2))
amount_ref = fields.Function(fields.Numeric('Amount Ref.',
digits=(6, 2)), 'get_amount_ref')
@classmethod
def __setup__(cls):
super(SaleLine, cls).__setup__()
cls.unit_price.states = {
'invisible': Eval('type') != 'line',
'required': Eval('type') == 'line',
'readonly': Eval('sale_state') == 'done'
}
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('production', None)
default.setdefault('charge_line', None)
return super(SaleLine, cls).copy(lines, default)
@fields.depends('quantity', 'unit')
def on_change_with_unit_qty(self, name=None):
if self.quantity and self.unit:
res = self.quantity * self.unit.factor
return res
@fields.depends('packing_qty', 'factor_packing')
def on_change_with_quantity(self):
if self.packing_qty and self.factor_packing:
return self.packing_qty * self.factor_packing
@fields.depends('packing_qty', 'packing_uom')
def on_change_with_boxes(self, name=None):
if self.packing_qty and self.packing_uom:
return self.packing_qty * self.packing_uom.factor
@fields.depends('kits', 'product', 'description')
def on_change_kits(self, name=None):
if self.product and self.kits:
string_ = [
ki.product.template.name
for ki in self.kits if ki.product and ki.product.template.farming]
if not string_:
return
desc_full = ' | '.join([self.product.rec_name] + string_)
self.description = desc_full
def get_amount_ref(self, name=None):
if self.unit_price_ref and self.quantity:
return Decimal(self.quantity) * self.unit_price_ref
def get_customer(self, name=None):
if self.sale:
return self.sale.party.id
def get_reference(self, name=None):
if self.sale:
return self.sale.reference
def _get_invoice_line_quantity(self):
qty = self.quantity
if self.sale.invoice_method != 'manual':
qty = super(SaleLine, self)._get_invoice_line_quantity()
return qty
class SaleLineKit(ModelSQL, ModelView):
"Sale Line Kit"
__name__ = "sale.line.kit"
line = fields.Many2One('sale.line', 'Line', required=True,
ondelete='CASCADE')
product = fields.Many2One('product.product', 'Product', required=True,
domain=[('type', '=', 'goods')])
uom = fields.Many2One('product.uom', 'UoM')
quantity = fields.Integer('Quantity', required=True)
notes = fields.Char('Notes')
components = fields.One2Many('sale.line.kit.component', 'kit', 'Components')
style = fields.Many2One('product.style', 'Style')
@fields.depends('product', 'uom')
def on_change_with_uom(self, name=None):
if self.product:
return self.product.default_uom.id
@staticmethod
def default_quantity():
return 1
class SaleLineKitComponent(ModelSQL, ModelView):
"Sale Line Component"
__name__ = "sale.line.kit.component"
kit = fields.Many2One('sale.line.kit', 'Kit', ondelete='CASCADE',
required=True)
product = fields.Many2One('product.product', 'Product', required=True,
domain=[('type', '=', 'goods')])
quantity = fields.Float('Quantity', digits=(2, 2))
notes = fields.Char('Notes')
style = fields.Many2One('product.style', 'Style')
@staticmethod
def default_quantity():
return 1
class SaleForceDraft(Wizard):
__name__ = 'sale_co.force_draft'
def transition_force_draft(self):
Sale = Pool().get('sale.sale')
Production = Pool().get('production')
ids = Transaction().context['active_ids']
if not ids:
return 'end'
sales = Sale.browse(ids)
for sale in sales:
for line in sale.lines:
productions = Production.search([
('origin', '=', str(line)),
])
for pd in productions:
if pd.state in ['assigned', 'running', 'done']:
raise UserError(
'No se puede borrar la produccion porque esta en proceso'
)
else:
Production.delete([pd])
super(SaleForceDraft, self).transition_force_draft()
return 'end'
class GroupingSalesStart(ModelView):
'Grouping Sales Start'
__name__ = 'sale.grouping_sales.start'
company = fields.Many2One('company.company', 'Company', required=True)
customer = fields.Many2One('party.party', 'Customer', required=True)
currency = fields.Many2One('currency.currency', 'Currency', required=True)
start_date = fields.Date("Start Date", required=True)
end_date = fields.Date("End Date", required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
class GroupingSales(Wizard):
'Grouping Sales'
__name__ = 'sale.grouping_sales'
start = StateView(
'sale.grouping_sales.start',
'farming.grouping_sales_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Ok', 'accept', 'tryton-ok', default=True),
])
accept = StateTransition()
def _create_invoice(self, lines, sale):
invoice_lines = []
for line in lines:
invoice_lines.append(line.get_invoice_line())
invoice_lines = list(chain(*invoice_lines))
if not invoice_lines:
return
invoice = sale._get_invoice_sale()
if getattr(invoice, 'lines', None):
invoice_lines = list(invoice.lines) + invoice_lines
invoice.lines = invoice_lines
invoice.save()
invoice.update_taxes()
sale.copy_resources_to(invoice)
return invoice
def transition_accept(self):
pool = Pool()
Sale = pool.get('sale.sale')
dom = ['OR', [
('state', 'in', ['confirmed', 'processing', 'done']),
('invoice_state', '=', 'none'),
('party', '=', self.start.customer.id),
('invoice_party', '=', None),
('currency', '=', self.start.currency.id),
('shipping_date', '>=', self.start.start_date),
('shipping_date', '<=', self.start.end_date),
], [
('state', 'in', ['confirmed', 'processing', 'done']),
('invoice_state', '=', 'none'),
('invoice_party', '=', self.start.customer.id),
('currency', '=', self.start.currency.id),
('shipping_date', '>=', self.start.start_date),
('shipping_date', '<=', self.start.end_date),
]]
sales = Sale.search(dom, order=[('shipping_date', 'ASC')])
if not sales:
return 'end'
lines_to_bill = []
for sale in sales:
if sale.invoices:
continue
for line in sale.lines:
lines_to_bill.append(line)
if lines_to_bill:
self._create_invoice(lines_to_bill, sales[0])
return 'end'
class ReturnSale(metaclass=PoolMeta):
__name__ = 'sale.return_sale'
def do_return_(self, action):
sales = self.records
return_sales = self.model.copy(sales)
for return_sale, sale in zip(return_sales, sales):
return_sale.origin = sale
for line in return_sale.lines:
line.production = None
if line.type == 'line':
line.quantity *= -1
return_sale.lines = return_sale.lines # Force saving
self.model.save(return_sales)
data = {'res_id': [s.id for s in return_sales]}
if len(return_sales) == 1:
action['views'].reverse()
return action, data
class SaleChangeProcessing(Wizard):
__name__ = 'sale.change_processing'
start_state = 'change_processing'
change_processing = StateTransition()
def transition_change_processing(self):
Sale = Pool().get('sale.sale')
sale_table = Table('sale_sale')
cursor = Transaction().connection.cursor()
sale_id = Transaction().context['active_id']
if not sale_id:
return 'end'
sale, = Sale.browse([sale_id])
if sale.state in ['done', 'processing'] and sale.id:
cursor.execute(*sale_table.update(
columns=[
sale_table.state,
sale_table.untaxed_amount_cache,
sale_table.tax_amount_cache,
sale_table.total_amount_cache],
values=['processing', 0, 0, 0],
where=sale_table.id == sale.id)
)
return 'end'
class PortfolioDetailedStart(ModelView):
'Portfolio Detailed Start'
__name__ = 'farming.portfolio_detailed.start'
company = fields.Many2One('company.company', 'Company', required=True)
to_date = fields.Date('To Date')
parties = fields.Many2Many('party.party', None, None, 'Parties')
type = fields.Selection([
('out', 'Customer'),
], 'Type', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_type():
return 'out'
class PortfolioDetailed(Wizard):
'Portfolio Detailed'
__name__ = 'farming.portfolio_detailed'
start = StateView(
'farming.portfolio_detailed.start',
'farming.print_sale_portfolio_detailed_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('farming.portfolio_detailed.report')
def do_print_(self, action):
parties_ids = [p.id for p in self.start.parties]
data = {
'company': self.start.company.id,
'type': self.start.type,
'to_date': self.start.to_date,
'parties': parties_ids,
}
return action, data
def transition_print_(self):
return 'end'
class PortfolioDetailedReport(Report):
__name__ = 'farming.portfolio_detailed.report'
@classmethod
def get_domain_inv(cls, dom_sales, data):
dom_sales.append([
('company', '=', data['company']),
])
if data['parties']:
dom_sales.append(
('party', 'in', data['parties'])
)
if data['to_date']:
dom_sales.append(
('shipping_date', '<=', data['to_date']),
)
return dom_sales
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Sale = pool.get('sale.sale')
dom_sales = []
dom_sales = cls.get_domain_inv(dom_sales, data)
sales = Sale.search(
dom_sales,
order=[('party.name', 'ASC'), ('shipping_date', 'ASC')]
)
salesmans = {}
for sale in sales:
pay_to_date = []
if data['to_date']:
for line in sale.payments:
if line.date <= data['to_date']:
pay_to_date.append(line.amount)
amount_to_pay = sale.total_amount - sum(pay_to_date)
else:
amount_to_pay = sale.residual_amount
pay_to_date = [
pay.amount for pay in sale.payments if pay.amount]
if sale.total_amount == sum(pay_to_date):
continue
if 'field_salesman' in data.keys() and \
data['field_salesman'] and sale.salesman:
if sale.salesman.id not in salesmans.keys():
salesmans[sale.salesman.id] = {
'salesman': sale.salesman.party.full_name,
'parties': {},
'total_invoices': [],
'total_amount_to_pay': [],
}
salesman = sale.salesman.id
elif 'without_seller' not in salesmans:
salesmans['without_seller'] = {
'salesman': 'without_seller',
'parties': {},
'total_invoices': [],
'total_amount_to_pay': [],
}
salesman = 'without_seller'
else:
salesman = 'without_seller'
if sale.party.id not in salesmans[salesman]['parties'].keys():
salesmans[salesman]['parties'][sale.party.id] = {
'party': sale.party,
'invoices': [],
'total_invoices': [],
'total_amount_to_pay': [],
}
party_key = salesmans[salesman]['parties'][sale.party.id]
salesmans[salesman]['total_invoices'].append(sale.total_amount)
salesmans[salesman]['total_amount_to_pay'].append(amount_to_pay)
party_key['invoices'].append(sale)
party_key['total_invoices'].append(sale.total_amount)
party_key['total_amount_to_pay'].append(amount_to_pay)
report_context['records'] = salesmans
report_context['data'] = data
return report_context
class AduanaDetailedStart(ModelView):
'Portfolio Detailed Start'
__name__ = 'farming.aduana_detailed.start'
date = fields.Date('date')
class AduanaDetailed(Wizard):
'Aduana Detailed'
__name__ = 'farming.aduana_detailed'
start = StateView(
'farming.aduana_detailed.start',
'farming.aduana_detailed_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('farming.aduana_detailed.report')
def do_print_(self, action):
data = {
'date': self.start.date,
}
return action, data
def transition_print_(self):
return 'end'
class AduanaDetailedReport(Report):
__name__ = 'farming.aduana_detailed.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Sale = pool.get('sale.sale')
sales = Sale.search(('shipping_date', '=', data['date']))
report_sales = {}
for sale in sales:
if sale.number:
total_weight = 0
for line in sale.lines:
if line.packing_uom.symbol == 'EB':
total_weight = total_weight + (line.packing_qty * 3.1)
elif line.packing_uom.symbol == 'QB':
total_weight = total_weight + (line.packing_qty * 7)
elif line.packing_uom.symbol == 'HB':
total_weight = total_weight + (line.packing_qty * 8.5)
report_sales[sale.number] = {
'client': sale.party.name,
'nit': sale.carrier.id_number,
'guia': sale.mawb,
'global': sale.custom_global and sale.custom_global.global_custom,
'invoice': sale.number,
'code': '0001',
'product': 'Hidrangea',
'units': sale.unit_qty,
'pieces': sale.packing_qty,
'weight': total_weight,
'total': sale.total_amount,
'license_plate': 'WHO-265',
}
report_context['records'] = report_sales.values()
return report_context