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) qty = forced_qty or qty_done # If the current stock is negative, we should not average it with the incoming one if float_is_zero(product_tot_qty_available, precision_rounding=rounding) or product_tot_qty_available < 0: 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, 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 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 write(self, vals): if 'uom_id' in vals: new_uom = self.env['uom.uom'].browse(vals['uom_id']) updated = self.filtered( lambda template: template.uom_id != new_uom) done_moves = self.env['stock.move'].search( [('product_id', 'in', updated.with_context( active_test=False).mapped('product_variant_ids').ids)], limit=1) if done_moves: raise UserError( _("You cannot change the unit of measure as there are already stock moves for this product. If you want to change the unit of measure, you should rather archive this product and create a new one." )) if 'type' in vals and vals['type'] != 'product' and sum( self.mapped('nbr_reordering_rules')) != 0: raise UserError( _('You still have some active reordering rules on this product. Please archive or delete them first.' )) if any('type' in vals and vals['type'] != prod_tmpl.type for prod_tmpl in self): existing_move_lines = self.env['stock.move.line'].search([ ('product_id', 'in', self.mapped('product_variant_ids').ids), ('state', 'in', ['partially_available', 'assigned']), ]) if existing_move_lines: raise UserError( _("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move." )) if 'type' in vals and vals['type'] != 'product' and self.filtered( lambda p: p.type == 'product' and not float_is_zero( p.qty_available, precision_rounding=p.uom_id.rounding)): raise UserError( _("Available quantity should be set to zero before changing type" )) return super(ProductTemplate, self).write(vals)
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 _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.user.company_id.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 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.action_invoice_open() # Check discount appeared on both SO lines and invoice lines for line, inv_line in pycompat.izip(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 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 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 _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 if not candidates: return False # 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 line_residual_to_compare = line_residual if line_residual > 0.0 else -line_residual total_residual_to_compare = total_residual if line_residual > 0.0 else -total_residual if line_residual_to_compare > total_residual_to_compare: amount_percentage = (total_residual_to_compare / line_residual_to_compare) * 100 elif total_residual: amount_percentage = (line_residual_to_compare / total_residual_to_compare) * 100 else: return False return amount_percentage >= self.match_total_amount_param
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 test_paid(self): if self.config_id.cash_rounding: total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) return float_is_zero(total - self.amount_paid, precision_rounding=self.config_id.currency_id.rounding) else: return super(PosOrder, self).test_paid()
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 Swerp 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 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 } prec = self.env['decimal.precision'].precision_get('Product Price') 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_digits=prec): 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: 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_product_to_produce( 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.final_lot_id = False wo.active_move_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 quantity > 0 and wo.move_raw_ids.filtered( lambda x: x.product_id.tracking != 'none' ) and not wo.active_move_line_ids: wo._generate_lot_ids() return {}
def _apply_rules(self, st_lines, excluded_ids=None, partner_map=None): ''' Apply criteria to get candidates for all reconciliation models. :param st_lines: Account.bank.statement.lines recordset. :param excluded_ids: Account.move.lines to exclude. :param partner_map: Dict mapping each line with new partner eventually. :return: A dict mapping each statement line id with: * aml_ids: A list of account.move.line ids. * model: An account.reconcile.model record (optional). * status: 'reconciled' if the lines has been already reconciled, 'write_off' if the write-off must be applied on the statement line. ''' available_models = self.filtered(lambda m: m.rule_type != 'writeoff_button') results = dict((r.id, {'aml_ids': []}) for r in st_lines) if not available_models: return results ordered_models = available_models.sorted(key=lambda m: (m.sequence, m.id)) grouped_candidates = {} # Type == 'invoice_matching'. # Map each (st_line.id, model_id) with matching amls. invoices_models = ordered_models.filtered(lambda m: m.rule_type == 'invoice_matching') if invoices_models: query, params = invoices_models._get_invoice_matching_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map) self._cr.execute(query, params) query_res = self._cr.dictfetchall() for res in query_res: grouped_candidates.setdefault(res['id'], {}) grouped_candidates[res['id']].setdefault(res['model_id'], []) grouped_candidates[res['id']][res['model_id']].append(res) # Type == 'writeoff_suggestion'. # Map each (st_line.id, model_id) with a flag indicating the st_line matches the criteria. write_off_models = ordered_models.filtered(lambda m: m.rule_type == 'writeoff_suggestion') if write_off_models: query, params = write_off_models._get_writeoff_suggestion_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map) self._cr.execute(query, params) query_res = self._cr.dictfetchall() for res in query_res: grouped_candidates.setdefault(res['id'], {}) grouped_candidates[res['id']].setdefault(res['model_id'], True) # Keep track of already processed amls. amls_ids_to_exclude = set() # Keep track of already reconciled amls. reconciled_amls_ids = set() # Iterate all and create results. for line in st_lines: line_currency = line.currency_id or line.journal_id.currency_id or line.company_id.currency_id line_residual = line.currency_id and line.amount_currency or line.amount # Search for applicable rule. # /!\ BREAK are very important here to avoid applying multiple rules on the same line. for model in ordered_models: # No result found. if not grouped_candidates.get(line.id) or not grouped_candidates[line.id].get(model.id): continue excluded_lines_found = False if model.rule_type == 'invoice_matching': candidates = grouped_candidates[line.id][model.id] # If some invoices match on the communication, suggest them. # Otherwise, suggest all invoices having the same partner. # N.B: The only way to match a line without a partner is through the communication. first_batch_candidates = [] second_batch_candidates = [] for c in candidates: # Don't take into account already reconciled lines. if c['aml_id'] in reconciled_amls_ids: continue # Dispatch candidates between lines matching invoices with the communication or only the partner. if c['communication_flag']: first_batch_candidates.append(c) elif not first_batch_candidates: second_batch_candidates.append(c) available_candidates = first_batch_candidates or second_batch_candidates # Special case: the amount are the same, submit the line directly. for c in available_candidates: residual_amount = c['aml_currency_id'] and c['aml_amount_residual_currency'] or c['aml_amount_residual'] if float_is_zero(residual_amount - line_residual, precision_rounding=line_currency.rounding): available_candidates = [c] break # Needed to handle check on total residual amounts. if first_batch_candidates or model._check_rule_propositions(line, available_candidates): results[line.id]['model'] = model # Add candidates to the result. for candidate in available_candidates: # Special case: the propositions match the rule but some of them are already consumed by # another one. Then, suggest the remaining propositions to the user but don't make any # automatic reconciliation. if candidate['aml_id'] in amls_ids_to_exclude: excluded_lines_found = True continue results[line.id]['aml_ids'].append(candidate['aml_id']) amls_ids_to_exclude.add(candidate['aml_id']) if excluded_lines_found: break # Create write-off lines. move_lines = self.env['account.move.line'].browse(results[line.id]['aml_ids']) partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id]) reconciliation_results = model._prepare_reconciliation(line, move_lines, partner=partner) # A write-off must be applied. if reconciliation_results['new_aml_dicts']: results[line.id]['status'] = 'write_off' # Process auto-reconciliation. if model.auto_reconcile: # An open balance is needed but no partner has been found. if reconciliation_results['open_balance_dict'] is False: break new_aml_dicts = reconciliation_results['new_aml_dicts'] if reconciliation_results['open_balance_dict']: new_aml_dicts.append(reconciliation_results['open_balance_dict']) if not line.partner_id and partner: line.partner_id = partner counterpart_moves = line.process_reconciliation( counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'], payment_aml_rec=reconciliation_results['payment_aml_rec'], new_aml_dicts=new_aml_dicts, ) results[line.id]['status'] = 'reconciled' results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids') # The reconciled move lines are no longer candidates for another rule. reconciled_amls_ids.update(move_lines.ids) # Break models loop. break elif model.rule_type == 'writeoff_suggestion' and grouped_candidates[line.id][model.id]: results[line.id]['model'] = model results[line.id]['status'] = 'write_off' # Create write-off lines. partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id]) reconciliation_results = model._prepare_reconciliation(line, partner=partner) # An open balance is needed but no partner has been found. if reconciliation_results['open_balance_dict'] is False: break # Process auto-reconciliation. if model.auto_reconcile: new_aml_dicts = reconciliation_results['new_aml_dicts'] if reconciliation_results['open_balance_dict']: new_aml_dicts.append(reconciliation_results['open_balance_dict']) if not line.partner_id and partner: line.partner_id = partner counterpart_moves = line.process_reconciliation( counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'], payment_aml_rec=reconciliation_results['payment_aml_rec'], new_aml_dicts=new_aml_dicts, ) results[line.id]['status'] = 'reconciled' results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids') # Break models loop. break return results
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" )
def _get_write_off_move_lines_dict(self, st_line, move_lines=None): ''' Get move.lines dict (to be passed to the create()) corresponding to the reconciliation model's write-off lines. :param st_line: An account.bank.statement.line record. :param move_lines: An account.move.line recordset. :return: A list of dict representing move.lines to be created corresponding to the write-off lines. ''' self.ensure_one() if self.rule_type == 'invoice_matching' and (not self.match_total_amount or (self.match_total_amount_param == 100)): return [] line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0 balance = total_residual - line_residual if not self.account_id or float_is_zero(balance, precision_rounding=line_currency.rounding): return [] if self.amount_type == 'percentage': line_balance = balance * (self.amount / 100.0) else: line_balance = self.amount * (1 if balance > 0.0 else -1) new_aml_dicts = [] # First write-off line. writeoff_line = { 'name': self.label or st_line.name, 'account_id': self.account_id.id, 'analytic_account_id': self.analytic_account_id.id, 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], 'debit': line_balance > 0 and line_balance or 0, 'credit': line_balance < 0 and -line_balance or 0, } new_aml_dicts.append(writeoff_line) if self.tax_id: writeoff_line['tax_ids'] = [(6, None, [self.tax_id.id])] tax = self.tax_id if self.force_tax_included: tax = tax.with_context(force_price_include=True) new_aml_dicts += self._get_taxes_move_lines_dict(tax, writeoff_line) # Second write-off line. if self.has_second_line and self.second_account_id: remaining_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts) if self.second_amount_type == 'percentage': line_balance = remaining_balance * (self.second_amount / 100.0) else: line_balance = self.second_amount * (1 if remaining_balance > 0.0 else -1) second_writeoff_line = { 'name': self.second_label or st_line.name, 'account_id': self.second_account_id.id, 'analytic_account_id': self.second_analytic_account_id.id, 'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)], 'debit': line_balance > 0 and line_balance or 0, 'credit': line_balance < 0 and -line_balance or 0, } new_aml_dicts.append(second_writeoff_line) if self.second_tax_id: second_writeoff_line['tax_ids'] = [(6, None, [self.second_tax_id.id])] tax = self.second_tax_id if self.force_second_tax_included: tax = tax.with_context(force_price_include=True) new_aml_dicts += self._get_taxes_move_lines_dict(tax, second_writeoff_line) return new_aml_dicts
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 ctx = self._context periods = {} date_from = fields.Date.from_string(date_from) start = date_from 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 = [] partner_clause = '' cr = self.env.cr user_company = self.env.user.company_id user_currency = user_company.currency_id 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), date_from, date_from, ) if ctx.get('partner_ids'): partner_clause = 'AND (l.partner_id IN %s)' arg_list += (tuple(ctx['partner_ids'].ids), ) if ctx.get('partner_categories'): partner_clause += 'AND (l.partner_id IN %s)' partner_ids = self.env['res.partner'].search([ ('category_id', 'in', ctx['partner_categories'].ids) ]).ids arg_list += (tuple(partner_ids or [0]), ) 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 ( l.reconciled IS FALSE OR l.id IN( SELECT credit_move_id FROM account_partial_reconcile where max_date > %s UNION ALL SELECT debit_move_id FROM account_partial_reconcile where max_date > %s ) ) ''' + partner_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 [], [], {} # 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 ORDER BY COALESCE(l.date_maturity, l.date)''' cr.execute(query, args_list) partners_amount = {} aml_ids = [x[0] for x in cr.fetchall()] # prefetch the fields that will be used; this avoid cache misses, # which look up the cache to determine the records to read, and has # quadratic complexity when the number of records is large... move_lines = self.env['account.move.line'].browse(aml_ids) move_lines.read([ 'partner_id', 'company_id', 'balance', 'matched_debit_ids', 'matched_credit_ids' ]) move_lines.mapped('matched_debit_ids').read( ['max_date', 'company_id', 'amount']) move_lines.mapped('matched_credit_ids').read( ['max_date', 'company_id', 'amount']) for line in move_lines: partner_id = line.partner_id.id or False if partner_id not in partners_amount: partners_amount[partner_id] = 0.0 line_amount = line.company_id.currency_id._convert( line.balance, user_currency, user_company, date_from) 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 += partial_line.company_id.currency_id._convert( partial_line.amount, user_currency, user_company, date_from) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= partial_line.company_id.currency_id._convert( partial_line.amount, user_currency, user_company, date_from) if not self.env.user.company_id.currency_id.is_zero( line_amount): partners_amount[partner_id] += line_amount lines.setdefault(partner_id, []) lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': i + 1, }) history.append(partners_amount) # 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 ORDER BY COALESCE(l.date_maturity, l.date)''' 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 = line.company_id.currency_id._convert( line.balance, user_currency, user_company, date_from) 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 += partial_line.company_id.currency_id._convert( partial_line.amount, user_currency, user_company, date_from) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= partial_line.company_id.currency_id._convert( partial_line.amount, user_currency, user_company, date_from) if not self.env.user.company_id.currency_id.is_zero(line_amount): undue_amounts[partner_id] += line_amount lines.setdefault(partner_id, []) lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': 6, }) 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']: #browse the partner name and trust field in sudo, as we may not have full access to the record (but we still have to see it in the report) browsed_partner = self.env['res.partner'].sudo().browse( partner['partner_id']) values['name'] = browsed_partner.name and len( browsed_partner.name) >= 45 and not self.env.context.get( 'no_format') and browsed_partner.name[ 0:41] + '...' 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 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 test_invoice_refund(self): """ Test invoice with a refund and check customer invoices credit note is created from respective invoice """ # 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") # Confirm the SO self.sale_order.action_confirm() # Check ordered quantity, quantity to invoice and invoiced quantity of SO lines for line in self.sale_order.order_line: if line.product_id.invoice_policy == 'delivery': self.assertEquals(line.qty_to_invoice, 0.0, 'Quantity to invoice should be same as ordered quantity') self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO') self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity") else: self.assertEquals(line.qty_to_invoice, line.product_uom_qty, 'Quantity to invoice should be same as ordered quantity') self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO') self.assertEquals(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line is confirmed") # 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] # Update quantity of an invoice lines invoice.invoice_line_ids[0].write({'quantity': 3.0}) # product ordered: from 5 to 3 invoice.invoice_line_ids[1].write({'quantity': 2.0}) # service ordered: from 3 to 2 # amount to invoice / invoiced should not have changed (amounts take only confirmed invoice into account) for line in self.sale_order.order_line: if line.product_id.invoice_policy == 'delivery': self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be zero") self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as delivered lines are not delivered yet") self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity (no confirmed invoice)") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now") else: if line == self.sol_prod_order: self.assertEquals(self.sol_prod_order.qty_to_invoice, 2.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines") self.assertEquals(self.sol_prod_order.qty_invoiced, 3.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines") else: self.assertEquals(self.sol_serv_order.qty_to_invoice, 1.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines") self.assertEquals(self.sol_serv_order.qty_invoiced, 2.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines") self.assertEquals(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed (no confirmed invoice)") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now") invoice.action_invoice_open() # Check quantity to invoice on SO lines for line in self.sale_order.order_line: if line.product_id.invoice_policy == 'delivery': self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity") self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO") self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity") else: if line == self.sol_prod_order: self.assertEquals(line.qty_to_invoice, 2.0, "The ordered sale line are totally invoiced (qty to invoice is zero)") self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)") else: self.assertEquals(line.qty_to_invoice, 1.0, "The ordered sale line are totally invoiced (qty to invoice is zero)") self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line are totally invoiced (qty invoiced = the invoice lines)") self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * line.qty_to_invoice, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products") self.assertEquals(line.untaxed_amount_invoiced, line.price_unit * line.qty_invoiced, "Amount invoiced is now set as qty invoiced * unit price since no price change on invoice, for ordered products") # Make a credit note credit_note_wizard = self.env['account.invoice.refund'].with_context({'active_ids': [invoice.id], 'active_id': invoice.id}).create({ 'filter_refund': 'modify', # this is the only mode for which the SO line is linked to the refund (https://github.com/swerp/swerp/commit/e680f29560ac20133c7af0c6364c6ef494662eac) 'description': 'reason test', }) credit_note_wizard.invoice_refund() invoice_2 = self.sale_order.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1] # the first invoice, its refund, and the new invoice # Check invoice's type and number self.assertEquals(invoice_2.type, 'out_invoice', 'The last created invoiced should be a customer invoice') self.assertEquals(invoice_2.state, 'draft', 'Last Customer invoices should be in draft') # At this time, the invoice 1 and its refund are confirmed, so the amounts invoiced are zero. The third invoice # (2nd customer inv) is in draft state. for line in self.sale_order.order_line: if line.product_id.invoice_policy == 'delivery': self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity") self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO") self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity") else: if line == self.sol_prod_order: self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)") self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line does not change on invoice 2 confirmation") self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 5, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied") else: self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)") self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line does not change on invoice 2 confirmation") self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 3, "Amount to invoice is now set as unit price * ordered qty - refund qty) even if the ") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied") # Change unit of ordered product on refund lines invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_prod_order.product_id).write({'price_unit': 100}) invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_serv_order.product_id).write({'price_unit': 50}) # Validate the refund invoice_2.action_invoice_open() for line in self.sale_order.order_line: if line.product_id.invoice_policy == 'delivery': self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity") self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO") self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity") self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity") else: if line == self.sol_prod_order: self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)") self.assertEquals(line.qty_invoiced, 3.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)") self.assertEquals(line.untaxed_amount_to_invoice, 1100.0, "") self.assertEquals(line.untaxed_amount_invoiced, 300.0, "") else: self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)") self.assertEquals(line.qty_invoiced, 2.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)") self.assertEquals(line.untaxed_amount_to_invoice, 170.0, "") self.assertEquals(line.untaxed_amount_invoiced, 100.0, "")
def _is_difference_zero(self): for bank_stmt in self: bank_stmt.is_difference_zero = float_is_zero( bank_stmt.difference, precision_digits=bank_stmt.currency_id.decimal_places)
def try_zero(amount, expected): self.assertEqual(float_is_zero(amount, precision_digits=3), expected, "Rounding error: %s should be zero!" % amount)