def check(self): """Check the order: if the order is not paid: continue payment, if the order is paid print ticket. """ self.ensure_one() order = self.env['pos.order'].browse( self.env.context.get('active_id', False)) currency = order.currency_id init_data = self.read()[0] if not float_is_zero(init_data['amount'], precision_rounding=currency.rounding): order.add_payment({ 'pos_order_id': order.id, 'amount': currency.round(init_data['amount']) if currency else init_data['amount'], 'name': init_data['payment_name'], 'payment_method_id': init_data['payment_method_id'][0], }) if float_is_zero(order.amount_total - order.amount_paid, precision_rounding=currency.rounding): order.action_pos_order_paid() return {'type': 'ir.actions.act_window_close'} return self.launch_payment()
def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered(lambda move: move._is_in() and move. product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[ move.product_id.id] rounding = move.product_id.uom_id.rounding qty_done = move.product_uom._compute_quantity( move.quantity_done, move.product_id.uom_id) if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ float_is_zero(product_tot_qty_available + qty_done, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or qty_done new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + qty) tmpl_dict[move.product_id.id] += qty_done # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_context( force_company=move.company_id.id).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price
def test_rounding_invalid(self): """ verify that invalid parameters are forbidden """ with self.assertRaises(AssertionError): float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_round(0.01, precision_digits=3, precision_rounding=0.01)
def _compute_average_price(self, qty_invoiced, qty_to_invoice, stock_moves): """Go over the valuation layers of `stock_moves` to value `qty_to_invoice` while taking care of ignoring `qty_invoiced`. If `qty_to_invoice` is greater than what's possible to value with the valuation layers, use the product's standard price. :param qty_invoiced: quantity already invoiced :param qty_to_invoice: quantity to invoice :param stock_moves: recordset of `stock.move` :returns: the anglo saxon price unit :rtype: float """ self.ensure_one() candidates = stock_moves\ .sudo()\ .mapped('stock_valuation_layer_ids')\ .sorted() qty_to_take_on_candidates = qty_to_invoice tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: candidate_quantity = abs(candidate.quantity) if not float_is_zero(qty_invoiced, precision_rounding=candidate.uom_id.rounding): qty_ignored = min(qty_invoiced, candidate_quantity) qty_invoiced -= qty_ignored candidate_quantity -= qty_ignored if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_quantity) qty_to_take_on_candidates -= qty_taken_on_candidate tmp_value += qty_taken_on_candidate * (candidate.value / candidate.quantity) if float_is_zero(qty_to_take_on_candidates, precision_rounding=candidate.uom_id.rounding): break # If there's still quantity to invoice but we're out of candidates, we chose the standard # price to estimate the anglo saxon price unit. if not float_is_zero(qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding): negative_stock_value = self.standard_price * qty_to_take_on_candidates tmp_value += negative_stock_value return tmp_value / qty_to_invoice
def _sale_get_invoice_price(self, order): """ Based on the current move line, compute the price to reinvoice the analytic line that is going to be created (so the price of the sale line). """ self.ensure_one() unit_amount = self.quantity amount = (self.credit or 0.0) - (self.debit or 0.0) if self.product_id.expense_policy == 'sales_price': return self.product_id.with_context( partner=order.partner_id.id, date_order=order.date_order, pricelist=order.pricelist_id.id, uom=self.product_uom_id.id).price uom_precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') if float_is_zero(unit_amount, precision_digits=uom_precision_digits): return 0.0 # Prevent unnecessary currency conversion that could be impacted by exchange rate # fluctuations if self.company_id.currency_id and amount and self.company_id.currency_id == order.currency_id: return abs(amount / unit_amount) price_unit = abs(amount / unit_amount) currency_id = self.company_id.currency_id if currency_id and currency_id != order.currency_id: price_unit = currency_id._convert( price_unit, order.currency_id, order.company_id, order.date_order or fields.Date.today()) return price_unit
def _check_rule_propositions(self, statement_line, candidates): ''' Check restrictions that can't be handled for each move.line separately. /!\ Only used by models having a type equals to 'invoice_matching'. :param statement_line: An account.bank.statement.line record. :param candidates: Fetched account.move.lines from query (dict). :return: True if the reconciliation propositions are accepted. False otherwise. ''' if not self.match_total_amount: return True # Match total residual amount. total_residual = 0.0 for aml in candidates: if aml['account_internal_type'] == 'liquidity': total_residual += aml['aml_currency_id'] and aml[ 'aml_amount_currency'] or aml['aml_balance'] else: total_residual += aml['aml_currency_id'] and aml[ 'aml_amount_residual_currency'] or aml[ 'aml_amount_residual'] line_residual = statement_line.currency_id and statement_line.amount_currency or statement_line.amount line_currency = statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.company_id.currency_id # Statement line amount is equal to the total residual. if float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding): return True if line_residual > total_residual: amount_percentage = (total_residual / line_residual) * 100 else: amount_percentage = (line_residual / total_residual) * 100 return amount_percentage >= self.match_total_amount_param
def _check_sum(self): """ Check if each cost line its valuation lines sum to the correct amount and if the overall total amount is correct also """ prec_digits = self.env.company.currency_id.decimal_places for landed_cost in self: total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits): return False val_to_cost_lines = defaultdict(lambda: 0.0) for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits) for cost_line, val_amount in val_to_cost_lines.items()): return False return True
def _search_difference_qty(self, operator, value): if operator == '=': result = True elif operator == '!=': result = False else: raise NotImplementedError() lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))]) line_ids = lines.filtered(lambda line: float_is_zero(line.difference_qty, line.product_id.uom_id.rounding) == result).ids return [('id', 'in', line_ids)]
def _compute_hide_line(self): for rec in self: report = rec.report_id r = (rec.currency_id or report.company_id.currency_id).rounding if report.hide_account_at_0 and ( float_is_zero(rec.initial_balance, precision_rounding=r) and float_is_zero(rec.final_balance, precision_rounding=r) and float_is_zero(rec.debit, precision_rounding=r) and float_is_zero(rec.credit, precision_rounding=r)): rec.hide_line = True elif report.limit_hierarchy_level and report.show_hierarchy_level: if report.hide_parent_hierarchy_level: distinct_level = rec.level != report.show_hierarchy_level if rec.account_group_id and distinct_level: rec.hide_line = True elif rec.level and distinct_level: rec.hide_line = True elif not report.hide_parent_hierarchy_level and \ rec.level > report.show_hierarchy_level: rec.hide_line = True
def create(self, vals_list): move_lines = super(StockMoveLine, self).create(vals_list) for move_line in move_lines: if move_line.state != 'done': continue move = move_line.move_id rounding = move.product_id.uom_id.rounding diff = move_line.qty_done if float_is_zero(diff, precision_rounding=rounding): continue self._create_correction_svl(move, diff) return move_lines
def write(self, vals): if 'qty_done' in vals: for move_line in self: if move_line.state != 'done': continue move = move_line.move_id rounding = move.product_id.uom_id.rounding diff = vals['qty_done'] - move_line.qty_done if float_is_zero(diff, precision_rounding=rounding): continue self._create_correction_svl(move, diff) return super(StockMoveLine, self).write(vals)
def _generate_moves(self): vals_list = [] for line in self: virtual_location = line._get_virtual_location() rounding = line.product_id.uom_id.rounding if float_is_zero(line.difference_qty, precision_rounding=rounding): continue if line.difference_qty > 0: # found more than expected vals = line._get_move_values(line.difference_qty, virtual_location.id, line.location_id.id, False) else: vals = line._get_move_values(abs(line.difference_qty), line.location_id.id, virtual_location.id, True) vals_list.append(vals) return self.env['stock.move'].create(vals_list)
def is_zero(self, amount): """Returns true if ``amount`` is small enough to be treated as zero according to current currency's rounding rules. Warning: ``is_zero(amount1-amount2)`` is not always equivalent to ``compare_amounts(amount1,amount2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. :param float amount: amount to compare with currency's zero With the new API, call it like: ``currency.is_zero(amount)``. """ return tools.float_is_zero(amount, precision_rounding=self.rounding)
def test_invoice_with_discount(self): """ Test invoice with a discount and check discount applied on both SO lines and an invoice lines """ # Update discount and delivered quantity on SO lines self.sol_prod_order.write({'discount': 20.0}) self.sol_serv_deliver.write({'discount': 20.0, 'qty_delivered': 4.0}) self.sol_serv_order.write({'discount': -10.0}) self.sol_prod_deliver.write({'qty_delivered': 2.0}) for line in self.sale_order.order_line.filtered(lambda l: l.discount): product_price = line.price_unit * line.product_uom_qty self.assertEquals(line.discount, (product_price - line.price_subtotal) / product_price * 100, 'Discount should be applied on order line') # lines are in draft for line in self.sale_order.order_line: self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state") self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state") self.sale_order.action_confirm() for line in self.sale_order.order_line: self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state") self.assertEquals(self.sol_serv_order.untaxed_amount_to_invoice, 297, "The untaxed amount to invoice is wrong") self.assertEquals(self.sol_serv_deliver.untaxed_amount_to_invoice, self.sol_serv_deliver.qty_delivered * self.sol_serv_deliver.price_reduce, "The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)") self.assertEquals(self.sol_prod_deliver.untaxed_amount_to_invoice, 140, "The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)") # Let's do an invoice with invoiceable lines payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({ 'advance_payment_method': 'delivered' }) payment.create_invoices() invoice = self.sale_order.invoice_ids[0] invoice.post() # Check discount appeared on both SO lines and invoice lines for line, inv_line in zip(self.sale_order.order_line, invoice.invoice_line_ids): self.assertEquals(line.discount, inv_line.discount, 'Discount on lines of order and invoice should be same')
def _costs_generate(self): """ Calculates total costs at the end of the production. """ self.ensure_one() AccountAnalyticLine = self.env['account.analytic.line'].sudo() for wc_line in self.workorder_ids.filtered( 'workcenter_id.costs_hour_account_id'): vals = self._prepare_wc_analytic_line(wc_line) precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding if not float_is_zero(vals.get('amount', 0.0), precision_rounding=precision_rounding): # we use SUPERUSER_ID as we do not guarantee an mrp user # has access to account analytic lines but still should be # able to produce orders AccountAnalyticLine.create(vals)
def _onchange_qty_done(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will warn him if he set `qty_done` to a non-supported value. """ res = {} if self.product_id.tracking == 'serial' and not float_is_zero( self.qty_done, self.product_uom_id.rounding): if float_compare( self.qty_done, 1.0, precision_rounding=self.product_uom_id.rounding) != 0: message = _( 'You can only process 1.0 %s of products with unique serial number.' ) % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res
def summary(self): res = super(EventRegistration, self).summary() if self.event_ticket_id.product_id.image_medium: res['image'] = '/web/image/product.product/%s/image_medium' % self.event_ticket_id.product_id.id information = res.setdefault('information', {}) information.append((_('Name'), self.name)) information.append((_('Ticket'), self.event_ticket_id.name or _('None'))) order = self.sale_order_id.sudo() order_line = self.sale_order_line_id.sudo() if not order or float_is_zero(order_line.price_total, precision_digits=order.currency_id.rounding): payment_status = _('Free') elif not order.invoice_ids or any(invoice.state != 'paid' for invoice in order.invoice_ids): payment_status = _('To pay') res['alert'] = _('The registration must be paid') else: payment_status = _('Paid') information.append((_('Payment'), payment_status)) return res
def check(self): """Check the order: if the order is not paid: continue payment, if the order is paid print ticket. """ self.ensure_one() order = self.env['pos.order'].browse(self.env.context.get('active_id', False)) currency = order.pricelist_id.currency_id amount = order.amount_total - order.amount_paid data = self.read()[0] # add_payment expect a journal key data['journal'] = data['journal_id'][0] data['amount'] = currency.round(data['amount']) if currency else data['amount'] if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01): order.add_payment(data) if order.test_paid(): order.action_pos_order_paid() return {'type': 'ir.actions.act_window_close'} return self.launch_payment()
def _create_out_svl(self, forced_quantity=None): """Create a `stock.valuation.layer` from `self`. :param forced_quantity: under some circunstances, the quantity to value is different than the initial demand of the move (Default value = None) """ svl_vals_list = [] for move in self: move = move.with_context(force_company=move.company_id.id) valued_move_lines = move._get_out_move_lines() valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id) if float_is_zero(forced_quantity or valued_quantity, precision_rounding=move.product_id.uom_id.rounding): continue svl_vals = move.product_id._prepare_out_svl_vals(forced_quantity or valued_quantity, move.company_id) svl_vals.update(move._prepare_common_svl_vals()) if forced_quantity: svl_vals['description'] = 'Correction of %s (modification of past move)' % move.picking_id.name or move.name svl_vals_list.append(svl_vals) return self.env['stock.valuation.layer'].sudo().create(svl_vals_list)
def _svl_empty_stock(self, description, product_category=None, product_template=None): impacted_product_ids = [] impacted_products = self.env['product.product'] products_orig_quantity_svl = {} # get the impacted products domain = [('type', '=', 'product')] if product_category is not None: domain += [('categ_id', '=', product_category.id)] elif product_template is not None: domain += [('product_tmpl_id', '=', product_template.id)] else: raise ValueError() products = self.env['product.product'].search_read( domain, ['quantity_svl']) for product in products: impacted_product_ids.append(product['id']) products_orig_quantity_svl[product['id']] = product['quantity_svl'] impacted_products |= self.env['product.product'].browse( impacted_product_ids) # empty out the stock for the impacted products empty_stock_svl_list = [] for product in impacted_products: # FIXME sle: why not use products_orig_quantity_svl here? if float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): # FIXME: create an empty layer to track the change? continue svsl_vals = product._prepare_out_svl_vals(product.quantity_svl, self.env.company) svsl_vals['description'] = description svsl_vals['company_id'] = self.env.company.id empty_stock_svl_list.append(svsl_vals) return empty_stock_svl_list, products_orig_quantity_svl, impacted_products
def action_sheet_move_create(self): if any(sheet.state != 'approve' for sheet in self): raise UserError( _("You can only generate accounting entry for approved expense(s)." )) if any(not sheet.journal_id for sheet in self): raise UserError( _("Expenses must have an expense journal specified to generate accounting entries." )) expense_line_ids = self.mapped('expense_line_ids')\ .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding)) res = expense_line_ids.action_move_create() if not self.accounting_date: self.accounting_date = self.account_move_id.date if self.payment_mode == 'own_account' and expense_line_ids: self.write({'state': 'post'}) else: self.write({'state': 'done'}) self.activity_update() return res
def do_change_standard_price(self, new_price, account_id): """ Changes the Standard Price of Product and creates an account move accordingly.""" AccountMove = self.env['account.move'] quant_locs = self.env['stock.quant'].sudo().read_group( [('product_id', 'in', self.ids)], ['location_id'], ['location_id']) quant_loc_ids = [loc['location_id'][0] for loc in quant_locs] locations = self.env['stock.location'].search([ ('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id), ('id', 'in', quant_loc_ids) ]) product_accounts = { product.id: product.product_tmpl_id.get_product_accounts() for product in self } for location in locations: for product in self.with_context( location=location.id, compute_child=False).filtered( lambda r: r.valuation == 'real_time'): diff = product.standard_price - new_price if float_is_zero( diff, precision_rounding=product.currency_id.rounding): raise UserError( _("No difference between the standard price and the new price." )) if not product_accounts[product.id].get( 'stock_valuation', False): raise UserError( _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.' )) qty_available = product.qty_available if qty_available: # Accounting Entries if diff * qty_available > 0: debit_account_id = account_id credit_account_id = product_accounts[ product.id]['stock_valuation'].id else: debit_account_id = product_accounts[ product.id]['stock_valuation'].id credit_account_id = account_id move_vals = { 'journal_id': product_accounts[product.id]['stock_journal'].id, 'company_id': location.company_id.id, 'ref': product.default_code, 'line_ids': [(0, 0, { 'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name), 'account_id': debit_account_id, 'debit': abs(diff * qty_available), 'credit': 0, 'product_id': product.id, }), (0, 0, { 'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name), 'account_id': credit_account_id, 'debit': 0, 'credit': abs(diff * qty_available), 'product_id': product.id, })], } move = AccountMove.create(move_vals) move.post() self.write({'standard_price': new_price}) return True
def change_prod_qty(self): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done')) if wizard.product_qty < produced: format_qty = '%.{precision}f'.format(precision=precision) raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced)) old_production_qty = production.product_qty production.write({'product_qty': wizard.product_qty}) done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id) qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id) factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) documents = {} for line, line_data in lines: if line.child_bom_id and line.child_bom_id.type == 'phantom' or\ line.product_id.type not in ['product', 'consu']: continue move = production.move_raw_ids.filtered(lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel')) if move: move = move[0] old_qty = move.product_uom_qty else: old_qty = 0 iterate_key = production._get_document_iterate_key(move) if iterate_key: document = self.env['stock.picking']._log_activity_get_documents({move: (line_data['qty'], old_qty)}, iterate_key, 'UP') for key, value in document.items(): if documents.get(key): documents[key] += [value] else: documents[key] = [value] production._update_raw_move(line, line_data) production._log_manufacture_exception(documents) operation_bom_qty = {} for bom, bom_data in boms: for operation in bom.routing_id.operation_ids: operation_bom_qty[operation.id] = bom_data['qty'] finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty) production._log_downside_manufactured_quantity(finished_moves_modification) moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves._action_assign() for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): cycle_number = float_round(operation_bom_qty[operation.id] / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP') wo.duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0 else: quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): wo.finished_lot_id = False wo._workorder_line_ids().unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' if wo.qty_produced == wo.qty_production and wo.state == 'progress': wo.state = 'done' # assign moves; last operation receive all unassigned moves # TODO: following could be put in a function as it is similar as code in _workorders_create # TODO: only needed when creating new moves moves_raw = production.move_raw_ids.filtered(lambda move: move.operation_id == operation and move.state not in ('done', 'cancel')) if wo == production.workorder_ids[-1]: moves_raw |= production.move_raw_ids.filtered(lambda move: not move.operation_id) moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) if wo.state not in ('done', 'cancel'): line_values = wo._update_workorder_lines() wo._workorder_line_ids().create(line_values['to_create']) if line_values['to_delete']: line_values['to_delete'].unlink() for line, vals in line_values['to_update'].items(): line.write(vals) return {}
def test_timesheet_manual(self): """ Test timesheet invoicing with 'invoice on delivery' timetracked products """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_customer_usd.id, 'partner_invoice_id': self.partner_customer_usd.id, 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_manual_global_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual2.name, 'product_id': self.product_delivery_manual2.id, 'product_uom_qty': 50, 'product_uom': self.product_delivery_manual2.uom_id.id, 'price_unit': self.product_delivery_manual2.list_price, 'order_id': sale_order.id, }) so_line_manual_only_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual4.name, 'product_id': self.product_delivery_manual4.id, 'product_uom_qty': 20, 'product_uom': self.product_delivery_manual4.uom_id.id, 'price_unit': self.product_delivery_manual4.list_price, 'order_id': sale_order.id, }) # confirm SO sale_order.action_confirm() self.assertTrue(sale_order.project_ids, "Sales Order should have create a project") self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: manually product should not need to be invoiced on so confirmation' ) project_serv2 = so_line_manual_only_project.project_id self.assertTrue( project_serv2, "A second project is created when selling 'project only' after SO confirmation." ) self.assertEqual( sale_order.analytic_account_id, project_serv2.analytic_account_id, "The created project should be linked to the analytic account of the SO" ) # let's log some timesheets (on task and project) timesheet1 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': self.project_global.id, # global project 'task_id': so_line_manual_global_project.task_id.id, 'unit_amount': 6, 'employee_id': self.employee_manager.id, }) timesheet2 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': self.project_global.id, # global project 'unit_amount': 3, 'employee_id': self.employee_manager.id, }) self.assertEqual( len(sale_order.project_ids), 2, "One project should have been created by the SO, when confirmed + the one coming from SO line 1 'task in global project'." ) self.assertEqual( so_line_manual_global_project.task_id.sale_line_id, so_line_manual_global_project, "Task from a milestone product should be linked to its SO line too" ) self.assertEqual( timesheet1.timesheet_invoice_type, 'billable_fixed', "Milestone timesheet goes in billable fixed category") self.assertTrue( float_is_zero(so_line_manual_global_project.qty_delivered, precision_digits=2), "Milestone Timesheeting should not incremented the delivered quantity on the SO line" ) self.assertEqual( so_line_manual_global_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created task." ) self.assertEqual( so_line_manual_only_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created project." ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation' ) self.assertEqual( timesheet1.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed since it is a milestone" ) self.assertEqual( timesheet2.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'") self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice") self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice") # invoice SO sale_order.order_line.write({'qty_delivered': 5}) invoice_id1 = sale_order.action_invoice_create() invoice1 = self.env['account.invoice'].browse(invoice_id1) for invoice_line in invoice1.invoice_line_ids: self.assertEqual( invoice_line.quantity, 5, "The invoiced quantity should be 5, as manually set on SO lines" ) self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since timesheets are used for time tracking in milestone" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since timesheets are used for time tracking in milestone" ) # validate the invoice invoice1.action_invoice_open() self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, even after invoice validation" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, even after invoice validation" )
def _get_partner_move_lines(self, account_type, date_from, target_move, period_length): # This method can receive the context key 'include_nullified_amount' {Boolean} # Do an invoice and a payment and unreconcile. The amount will be nullified # By default, the partner wouldn't appear in this report. # The context key allow it to appear # In case of a period_length of 30 days as of 2019-02-08, we want the following periods: # Name Stop Start # 1 - 30 : 2019-02-07 - 2019-01-09 # 31 - 60 : 2019-01-08 - 2018-12-10 # 61 - 90 : 2018-12-09 - 2018-11-10 # 91 - 120 : 2018-11-09 - 2018-10-11 # +120 : 2018-10-10 periods = {} start = datetime.strptime(date_from, "%Y-%m-%d") date_from = datetime.strptime(date_from, "%Y-%m-%d").date() for i in range(5)[::-1]: stop = start - relativedelta(days=period_length) period_name = str((5 - (i + 1)) * period_length + 1) + '-' + str( (5 - i) * period_length) period_stop = (start - relativedelta(days=1)).strftime('%Y-%m-%d') if i == 0: period_name = '+' + str(4 * period_length) periods[str(i)] = { 'name': period_name, 'stop': period_stop, 'start': (i != 0 and stop.strftime('%Y-%m-%d') or False), } start = stop res = [] total = [] cr = self.env.cr user_company = self.env.user.company_id user_currency = user_company.currency_id ResCurrency = self.env['res.currency'].with_context(date=date_from) company_ids = self._context.get('company_ids') or [user_company.id] move_state = ['draft', 'posted'] if target_move == 'posted': move_state = ['posted'] arg_list = (tuple(move_state), tuple(account_type)) #build the reconciliation clause to see what partner needs to be printed reconciliation_clause = '(l.reconciled IS FALSE)' cr.execute( 'SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where max_date > %s', (date_from, )) reconciled_after_date = [] for row in cr.fetchall(): reconciled_after_date += [row[0], row[1]] if reconciled_after_date: reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)' arg_list += (tuple(reconciled_after_date), ) arg_list += (date_from, tuple(company_ids)) query = ''' SELECT DISTINCT l.partner_id, UPPER(res_partner.name) FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND ''' + reconciliation_clause + ''' AND (l.date <= %s) AND l.company_id IN %s ORDER BY UPPER(res_partner.name)''' cr.execute(query, arg_list) partners = cr.dictfetchall() # put a total of 0 for i in range(7): total.append(0) # Build a string like (1,2,3) for easy use in SQL query partner_ids = [ partner['partner_id'] for partner in partners if partner['partner_id'] ] lines = dict( (partner['partner_id'] or False, []) for partner in partners) if not partner_ids: return [], [], {} # This dictionary will store the not due amount of all partners undue_amounts = {} query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND (COALESCE(l.date_maturity,l.date) >= %s)\ AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND (l.date <= %s) AND l.company_id IN %s''' cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, tuple(company_ids))) aml_ids = cr.fetchall() aml_ids = aml_ids and [x[0] for x in aml_ids] or [] for line in self.env['account.move.line'].browse(aml_ids): partner_id = line.partner_id.id or False if partner_id not in undue_amounts: undue_amounts[partner_id] = 0.0 line_amount = ResCurrency._compute(line.company_id.currency_id, user_currency, line.balance) if user_currency.is_zero(line_amount): continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: line_amount += ResCurrency._compute( partial_line.company_id.currency_id, user_currency, partial_line.amount) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= ResCurrency._compute( partial_line.company_id.currency_id, user_currency, partial_line.amount) if not self.env.user.company_id.currency_id.is_zero(line_amount): undue_amounts[partner_id] += line_amount lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': 6, }) # Use one query per period and store results in history (a list variable) # Each history will contain: history[1] = {'<partner_id>': <partner_debit-credit>} history = [] for i in range(5): args_list = ( tuple(move_state), tuple(account_type), tuple(partner_ids), ) dates_query = '(COALESCE(l.date_maturity,l.date)' if periods[str(i)]['start'] and periods[str(i)]['stop']: dates_query += ' BETWEEN %s AND %s)' args_list += (periods[str(i)]['start'], periods[str(i)]['stop']) elif periods[str(i)]['start']: dates_query += ' >= %s)' args_list += (periods[str(i)]['start'], ) else: dates_query += ' <= %s)' args_list += (periods[str(i)]['stop'], ) args_list += (date_from, tuple(company_ids)) query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND ''' + dates_query + ''' AND (l.date <= %s) AND l.company_id IN %s''' cr.execute(query, args_list) partners_amount = {} aml_ids = cr.fetchall() aml_ids = aml_ids and [x[0] for x in aml_ids] or [] for line in self.env['account.move.line'].browse(aml_ids): partner_id = line.partner_id.id or False if partner_id not in partners_amount: partners_amount[partner_id] = 0.0 line_amount = ResCurrency._compute(line.company_id.currency_id, user_currency, line.balance) if user_currency.is_zero(line_amount): continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: line_amount += ResCurrency._compute( partial_line.company_id.currency_id, user_currency, partial_line.amount) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= ResCurrency._compute( partial_line.company_id.currency_id, user_currency, partial_line.amount) if not self.env.user.company_id.currency_id.is_zero( line_amount): partners_amount[partner_id] += line_amount lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': i + 1, }) history.append(partners_amount) for partner in partners: if partner['partner_id'] is None: partner['partner_id'] = False at_least_one_amount = False values = {} undue_amt = 0.0 if partner[ 'partner_id'] in undue_amounts: # Making sure this partner actually was found by the query undue_amt = undue_amounts[partner['partner_id']] total[6] = total[6] + undue_amt values['direction'] = undue_amt if not float_is_zero(values['direction'], precision_rounding=self.env.user.company_id. currency_id.rounding): at_least_one_amount = True for i in range(5): during = False if partner['partner_id'] in history[i]: during = [history[i][partner['partner_id']]] # Adding counter total[(i)] = total[(i)] + (during and during[0] or 0) values[str(i)] = during and during[0] or 0.0 if not float_is_zero(values[str(i)], precision_rounding=self.env.user. company_id.currency_id.rounding): at_least_one_amount = True values['total'] = sum([values['direction']] + [values[str(i)] for i in range(5)]) ## Add for total total[(i + 1)] += values['total'] values['partner_id'] = partner['partner_id'] if partner['partner_id']: browsed_partner = self.env['res.partner'].browse( partner['partner_id']) values['name'] = browsed_partner.name and len( browsed_partner.name) >= 45 and browsed_partner.name[ 0:40] + '...' or browsed_partner.name values['trust'] = browsed_partner.trust else: values['name'] = _('Unknown Partner') values['trust'] = False if at_least_one_amount or ( self._context.get('include_nullified_amount') and lines[partner['partner_id']]): res.append(values) return res, total, lines
def write(self, vals): """ When editing a done stock.move.line, we impact the valuation. Users may increase or decrease the `qty_done` field. There are three cost method available: standard, average and fifo. We implement the logic in a similar way for standard and average: increase or decrease the original value with the standard or average price of today. In fifo, we have a different logic wheter the move is incoming or outgoing. If the move is incoming, we update the value and remaining_value/qty with the unit price of the move. If the move is outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in the stack the same way a new outgoing move would have done. If the move is outoing and the user decreases qty_done, we either increase the last receipt candidate if one is found or we decrease the value with the last fifo price. """ if 'qty_done' in vals: moves_to_update = {} for move_line in self.filtered(lambda ml: ml.state == 'done' and ( ml.move_id._is_in() or ml.move_id._is_out())): rounding = move_line.product_uom_id.rounding qty_difference = float_round(vals['qty_done'] - move_line.qty_done, precision_rounding=rounding) if not float_is_zero(qty_difference, precision_rounding=rounding): moves_to_update[move_line.move_id] = qty_difference for move_id, qty_difference in moves_to_update.items(): move_vals = {} if move_id.product_id.cost_method in ['standard', 'average']: correction_value = qty_difference * move_id.product_id.standard_price if move_id._is_in(): move_vals['value'] = move_id.value + correction_value elif move_id._is_out(): move_vals['value'] = move_id.value - correction_value else: if move_id._is_in(): correction_value = qty_difference * move_id.price_unit new_remaining_value = move_id.remaining_value + correction_value move_vals['value'] = move_id.value + correction_value move_vals[ 'remaining_qty'] = move_id.remaining_qty + qty_difference move_vals[ 'remaining_value'] = move_id.remaining_value + correction_value elif move_id._is_out() and qty_difference > 0: correction_value = self.env['stock.move']._run_fifo( move_id, quantity=qty_difference) # no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it move_vals['value'] = move_id.value - correction_value elif move_id._is_out() and qty_difference < 0: candidates_receipt = self.env['stock.move'].search( move_id._get_in_domain(), order='date, id desc', limit=1) if candidates_receipt: candidates_receipt.write({ 'remaining_qty': candidates_receipt.remaining_qty + -qty_difference, 'remaining_value': candidates_receipt.remaining_value + (-qty_difference * candidates_receipt.price_unit), }) correction_value = qty_difference * candidates_receipt.price_unit else: correction_value = qty_difference * move_id.product_id.standard_price move_vals['value'] = move_id.value - correction_value move_id.write(move_vals) if move_id.product_id.valuation == 'real_time': move_id.with_context( force_valuation_amount=correction_value, forced_quantity=qty_difference)._account_entry_move() if qty_difference > 0: move_id.product_price_update_before_done( forced_qty=qty_difference) return super(StockMoveLine, self).write(vals)
def try_zero(amount, expected): self.assertEqual(float_is_zero(amount, precision_digits=3), expected, "Rounding error: %s should be zero!" % amount)
def test_timesheet_order(self): """ Test timesheet invoicing with 'invoice on order' timetracked products 1. create SO with 2 ordered product and confirm 2. create invoice 3. log timesheet 4. add new SO line (ordered service) 5. create new invoice """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_customer_usd.id, 'partner_invoice_id': self.partner_customer_usd.id, 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_ordered_project_only = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet4.name, 'product_id': self.product_order_timesheet4.id, 'product_uom_qty': 10, 'product_uom': self.product_order_timesheet4.uom_id.id, 'price_unit': self.product_order_timesheet4.list_price, 'order_id': sale_order.id, }) so_line_ordered_global_project = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet2.name, 'product_id': self.product_order_timesheet2.id, 'product_uom_qty': 50, 'product_uom': self.product_order_timesheet2.uom_id.id, 'price_unit': self.product_order_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_ordered_project_only.product_id_change() so_line_ordered_global_project.product_id_change() sale_order.action_confirm() task_serv2 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_ordered_global_project.id) ]) project_serv1 = self.env['project.project'].search([ ('sale_line_id', '=', so_line_ordered_project_only.id) ]) self.assertEqual( sale_order.tasks_count, 1, "One task should have been created on SO confirmation") self.assertEqual( len(sale_order.project_ids), 2, "One project should have been created by the SO, when confirmed + the one from SO line 2 'task in global project'" ) self.assertEqual( sale_order.analytic_account_id, project_serv1.analytic_account_id, "The created project should be linked to the analytic account of the SO" ) # create invoice invoice_id1 = sale_order.action_invoice_create()[0] invoice1 = self.env['account.invoice'].browse(invoice_id1) # let's log some timesheets (on the project created by so_line_ordered_project_only) timesheet1 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, 'task_id': task_serv2.id, 'unit_amount': 10.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_global_project.qty_delivered, 10.5, 'Timesheet directly on project does not increase delivered quantity on so line' ) self.assertEqual( sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) self.assertEqual( timesheet1.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed" ) self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity" ) timesheet2 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, 'task_id': task_serv2.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_global_project.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line' ) self.assertEqual( sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) self.assertEqual( timesheet2.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity" ) timesheet3 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, 'unit_amount': 10, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_project_only.qty_delivered, 0.0, 'Timesheet directly on project does not increase delivered quantity on so line' ) self.assertEqual( timesheet3.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'") self.assertFalse( timesheet3.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity" ) # log timesheet on task in global project (higher than the initial ordrered qty) timesheet4 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, 'task_id': task_serv2.id, 'unit_amount': 5, 'employee_id': self.employee_user.id, }) self.assertEqual( sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity" ) # add so line with produdct "create task in new project". so_line_ordered_task_new_project = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 3, 'product_uom': self.product_order_timesheet3.uom_id.id, 'price_unit': self.product_order_timesheet3.list_price, 'order_id': sale_order.id, }) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: Adding a new service line (so line) should put the SO in "to invocie" state.' ) self.assertEqual( sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation" ) self.assertEqual( len(sale_order.project_ids), 2, "No new project should have been created by the SO, when selling 'new task in new project' product, since it reuse the one from 'project only'." ) # get first invoice line of sale line linked to timesheet1 invoice_line_1 = so_line_ordered_global_project.invoice_lines.filtered( lambda line: line.invoice_id.id == invoice_id1) self.assertEqual( so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when creating timesheet" ) # timesheet can be modified timesheet1.write({'unit_amount': 12}) self.assertEqual( so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when modifying timesheet" ) # create second invoice invoice_id2 = sale_order.action_invoice_create()[0] invoice2 = self.env['account.invoice'].browse(invoice_id2) self.assertEqual( len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO") self.assertTrue( float_is_zero(invoice2.amount_total - so_line_ordered_task_new_project.price_unit * 3, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity" ) # validate the first invoice invoice1.action_invoice_open() self.assertEqual( so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when modifying timesheet" ) self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity" ) # timesheet can still be modified timesheet1.write({'unit_amount': 13})
def generate_fec(self): self.ensure_one() # We choose to implement the flat file instead of the XML # file for 2 reasons : # 1) the XSD file impose to have the label on the account.move # but Eagle has the label on the account.move.line, so that's a # problem ! # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration company = self.env.user.company_id company_legal_data = self._get_company_legal_data(company) header = [ u'JournalCode', # 0 u'JournalLib', # 1 u'EcritureNum', # 2 u'EcritureDate', # 3 u'CompteNum', # 4 u'CompteLib', # 5 u'CompAuxNum', # 6 We use partner.id u'CompAuxLib', # 7 u'PieceRef', # 8 u'PieceDate', # 9 u'EcritureLib', # 10 u'Debit', # 11 u'Credit', # 12 u'EcritureLet', # 13 u'DateLet', # 14 u'ValidDate', # 15 u'Montantdevise', # 16 u'Idevise', # 17 ] rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref( 'account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once if unaffected_earnings_xml_ref: #compute the benefit/loss of last year to add in the initial balance of the current year earnings account unaffected_earnings_results = self.do_query_unaffected_earnings() unaffected_earnings_line = False sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type HAVING round(sum(aml.balance), %s) != 0 AND aat.type not in ('receivable', 'payable') ''' formatted_date_from = fields.Date.to_string(self.date_from).replace( '-', '') date_from = self.date_from formatted_date_year = date_from.year currency_digits = 2 self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() if not unaffected_earnings_line: account = self.env['account.account'].browse(account_id) if account.user_type_id.id == self.env.ref( 'account.data_unaffected_earnings').id: #add the benefit/loss of previous fiscal year to the first unaffected earnings account found. unaffected_earnings_line = True current_amount = float(listrow[11].replace( ',', '.')) - float(listrow[12].replace(',', '.')) unaffected_earnings_amount = float( unaffected_earnings_results[11].replace( ',', '.')) - float( unaffected_earnings_results[12].replace( ',', '.')) listrow_amount = current_amount + unaffected_earnings_amount if float_is_zero(listrow_amount, precision_digits=currency_digits): continue if listrow_amount > 0: listrow[11] = str(listrow_amount).replace('.', ',') listrow[12] = '0,00' else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') rows_to_write.append(listrow) #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results and (unaffected_earnings_results[11] != '0,00' or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account unaffected_earnings_account = self.env['account.account'].search( [('user_type_id', '=', self.env.ref('account.data_unaffected_earnings').id)], limit=1) if unaffected_earnings_account: unaffected_earnings_results[ 4] = unaffected_earnings_account.code unaffected_earnings_results[ 5] = unaffected_earnings_account.name rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN(aa.name), '|', '/') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type, rp.ref, rp.id HAVING round(sum(aml.balance), %s) != 0 AND aat.type in ('receivable', 'payable') ''' self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() rows_to_write.append(listrow) # LINES sql_query = ''' SELECT replace(replace(aj.code, '|', '/'), '\t', '') AS JournalCode, replace(replace(aj.name, '|', '/'), '\t', '') AS JournalLib, replace(replace(am.name, '|', '/'), '\t', '') AS EcritureNum, TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate, aa.code AS CompteNum, replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib, CASE WHEN am.ref IS null OR am.ref = '' THEN '-' ELSE replace(replace(am.ref, '|', '/'), '\t', '') END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/' ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet, TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate, CASE WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN '' ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',') END AS Montantdevise, CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_journal aj ON aj.id = am.journal_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id WHERE am.date >= %s AND am.date <= %s AND am.company_id = %s AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' ORDER BY am.date, am.name, aml.id ''' self._cr.execute(sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): rows_to_write.append(list(row)) fecvalue = self._csv_write_rows(rows_to_write) end_date = fields.Date.to_string(self.date_to).replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' self.write({ 'fec_data': base64.encodestring(fecvalue), # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date 'filename': '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix), }) action = { 'name': 'FEC', 'type': 'ir.actions.act_url', 'url': "web/content/?model=account.fr.fec&id=" + str(self.id) + "&filename_field=filename&field=fec_data&download=true&filename=" + self.filename, 'target': 'self', } return action
def test_timesheet_delivery(self): """ Test timesheet invoicing with 'invoice on delivery' timetracked products 1. Create SO and confirm it 2. log timesheet 3. create invoice 4. log other timesheet 5. create a second invoice 6. add new SO line (delivered service) """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_customer_usd.id, 'partner_invoice_id': self.partner_customer_usd.id, 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_deliver_global_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, 'product_uom': self.product_delivery_timesheet2.uom_id.id, 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_deliver_task_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 20, 'product_uom': self.product_delivery_timesheet3.uom_id.id, 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) so_line_deliver_global_project.product_id_change() so_line_deliver_task_project.product_id_change() # confirm SO sale_order.action_confirm() task_serv1 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_deliver_global_project.id) ]) task_serv2 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_deliver_task_project.id) ]) project_serv2 = self.env['project.project'].search([ ('sale_line_id', '=', so_line_deliver_task_project.id) ]) self.assertEqual( task_serv1.project_id, self.project_global, "Sale Timesheet: task should be created in global project") self.assertTrue( task_serv1, "Sale Timesheet: on SO confirmation, a task should have been created in global project" ) self.assertTrue( task_serv2, "Sale Timesheet: on SO confirmation, a task should have been created in a new project" ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation' ) self.assertEqual(sale_order.analytic_account_id, task_serv2.project_id.analytic_account_id, "SO should have create a project") self.assertEqual( sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation" ) self.assertEqual( len(sale_order.project_ids), 2, "One project should have been created by the SO, when confirmed + the one from SO line 1 'task in global project'" ) self.assertEqual( sale_order.analytic_account_id, project_serv2.analytic_account_id, "The created project should be linked to the analytic account of the SO" ) # let's log some timesheets timesheet1 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv1.project_id.id, # global project 'task_id': task_serv1.id, 'unit_amount': 10.5, 'employee_id': self.employee_manager.id, }) self.assertEqual( so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line' ) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged' ) self.assertEqual( timesheet1.timesheet_invoice_type, 'billable_time', "Timesheets linked to SO line with delivered product shoulbe be billable time" ) self.assertFalse( timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice yet") # invoice SO invoice_id1 = sale_order.action_invoice_create() invoice1 = self.env['account.invoice'].browse(invoice_id1) self.assertTrue( float_is_zero(invoice1.amount_total - so_line_deliver_global_project.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') self.assertEqual( timesheet1.timesheet_invoice_id, invoice1, "The timesheet1 should not be linked to the invoice 1, as we are in delivered quantity (even if invoice is in draft" ) with self.assertRaises( UserError ): # We can not modify timesheet linked to invoice (even draft ones) timesheet1.write({'unit_amount': 42}) # log some timesheets again timesheet2 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv1.project_id.id, # global project 'task_id': task_serv1.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line' ) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so' ) self.assertEqual( timesheet2.timesheet_invoice_type, 'billable_time', "Timesheets linked to SO line with delivered product shoulbe be billable time" ) self.assertFalse( timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice yet") # create a second invoice invoice_id2 = sale_order.action_invoice_create()[0] invoice2 = self.env['account.invoice'].browse(invoice_id2) self.assertEqual( len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO") self.assertEqual( so_line_deliver_global_project.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now' ) self.assertEqual( timesheet2.timesheet_invoice_id, invoice2, "The timesheet2 should not be linked to the invoice 2") with self.assertRaises( UserError ): # We can not modify timesheet linked to invoice (even draft ones) timesheet2.write({'unit_amount': 42}) # add a line on SO so_line_deliver_only_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet4.name, 'product_id': self.product_delivery_timesheet4.id, 'product_uom_qty': 5, 'product_uom': self.product_delivery_timesheet4.uom_id.id, 'price_unit': self.product_delivery_timesheet4.list_price, 'order_id': sale_order.id, }) self.assertEqual( len(sale_order.project_ids), 2, "No new project should have been created by the SO, when selling 'project only' product, since it reuse the one from 'new task in new project'." ) # let's log some timesheets on the project timesheet3 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': project_serv2.id, 'unit_amount': 7, 'employee_id': self.employee_user.id, }) self.assertTrue( float_is_zero(so_line_deliver_only_project.qty_delivered, precision_digits=2), "Timesheeting on project should not incremented the delivered quantity on the SO line" ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now' ) self.assertEqual( timesheet3.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'") self.assertFalse( timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice yet") # let's log some timesheets on the task (new task/new project) timesheet4 = self.env['account.analytic.line'].create({ 'name': 'Test Line 4', 'project_id': task_serv2.project_id.id, 'task_id': task_serv2.id, 'unit_amount': 7, 'employee_id': self.employee_user.id, }) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice yet") # modify a non invoiced timesheet timesheet4.write({'unit_amount': 42}) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet4 should not still be linked to the invoice") # validate the second invoice invoice2.action_invoice_open() self.assertEqual( timesheet1.timesheet_invoice_id, invoice1, "The timesheet1 should not be linked to the invoice 1, even after validation" ) self.assertEqual( timesheet2.timesheet_invoice_id, invoice2, "The timesheet2 should not be linked to the invoice 1, even after validation" ) self.assertFalse( timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity" ) self.assertFalse( timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity" )