diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index bd0c5e30..9d766a7b 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -203,8 +203,12 @@ class PurchaseOrder(models.Model): self.payment_term_id = payment_term.id, self.company_id = requisition.company_id.id self.currency_id = currency.id - self.origin = requisition.name - self.partner_ref = requisition.name # to control vendor bill based on agreement reference + if not self.origin or requisition.name not in self.origin.split(', '): + if self.origin: + if requisition.name: + self.origin = self.origin + ', ' + requisition.name + else: + self.origin = requisition.name self.notes = requisition.description self.date_order = requisition.date_end or fields.Datetime.now() self.picking_type_id = requisition.picking_type_id.id diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 424e6d2a..80fe1939 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -18,6 +18,8 @@ from flectra.tools.float_utils import float_compare def float_to_time(float_hour): + if float_hour == 24.0: + return datetime.time.max return datetime.time(int(math.modf(float_hour)[1]), int(60 * math.modf(float_hour)[0]), 0) @@ -643,7 +645,9 @@ class ResourceCalendarAttendance(models.Model): ], 'Day of Week', required=True, index=True, default='0') date_from = fields.Date(string='Starting Date') date_to = fields.Date(string='End Date') - hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.") + hour_from = fields.Float(string='Work from', required=True, index=True, + help="Start and End time of working.\n" + "A specific value of 24:00 is interpreted as 23:59:59.999999.") hour_to = fields.Float(string='Work to', required=True) calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade') diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index e6c2e1cd..c8dec692 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -243,6 +243,21 @@ class ResourceWorkingHours(TestResourceCommon): compute_leaves=True) self.assertEqual(res, 33.0) + def test_calendar_working_hours_24(self): + self.att_4 = self.env['resource.calendar.attendance'].create({ + 'name': 'Att4', + 'calendar_id': self.calendar.id, + 'dayofweek': '2', + 'hour_from': 0, + 'hour_to': 24 + }) + res = self.calendar.get_work_hours_count( + Datetime.from_string('2018-06-19 23:00:00'), + Datetime.from_string('2018-06-21 01:00:00'), + self.resource1_id, + compute_leaves=True) + self.assertAlmostEqual(res, 24.0) + def test_calendar_timezone(self): # user in timezone UTC-9 asks for work hours # Limits: between 2013-02-19 10:00:00 and 2013-02-26 15:30:00 (User TZ) diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index 0ea32111..6b67047d 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -12,6 +12,8 @@ This module contains all the common features of Sales Management and eCommerce. """, 'depends': ['sales_team', 'account', 'portal'], 'data': [ + 'security/sale_security.xml', + 'security/ir.model.access.csv', 'data/ir_sequence_data.xml', 'data/sale_data.xml', 'report/sale_report.xml', @@ -20,8 +22,6 @@ This module contains all the common features of Sales Management and eCommerce. 'report/sale_report_templates.xml', 'report/invoice_report_templates.xml', 'report/report_all_channels_sales_views.xml', - 'security/sale_security.xml', - 'security/ir.model.access.csv', 'wizard/sale_make_invoice_advance_views.xml', 'views/sale_views.xml', 'views/account_invoice_views.xml', diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml index 24890149..5b18666b 100644 --- a/addons/sale/data/mail_template_data.xml +++ b/addons/sale/data/mail_template_data.xml @@ -5,7 +5,7 @@ Sales Order - Send by Email - ${(object.user_id.email and '%s <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} + ${(object.user_id.email and '"%s" <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} ${object.company_id.name} ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} (Ref ${object.name or 'n/a' }) ${object.partner_id.id} @@ -70,7 +70,7 @@ from ${object.company_id.name}. % set record = ctx.get('record') - % set company = record and record.company_id or user.company_id + % set company = record and record.company_id or ctx.get('company') diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index f8ad070e..8d3fdff9 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -39,6 +39,9 @@ class AccountInvoice(models.Model): def _onchange_delivery_address(self): addr = self.partner_id.address_get(['delivery']) self.partner_shipping_id = addr and addr.get('delivery') + if self.env.context.get('type', 'out_invoice') == 'out_invoice': + company = self.company_id or self.env.user.company_id + self.comment = company.with_context(lang=self.partner_id.lang).sale_note @api.multi def action_invoice_paid(self): @@ -96,15 +99,6 @@ class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' _order = 'invoice_id, layout_category_id, sequence, id' - @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', - 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id', - 'invoice_id.date_invoice') - def _compute_total_price(self): - for line in self: - price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - taxes = line.invoice_line_tax_ids.compute_all(price, line.invoice_id.currency_id, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id) - line.price_total = taxes['total_included'] - sale_line_ids = fields.Many2many( 'sale.order.line', 'sale_order_line_invoice_rel', @@ -113,4 +107,3 @@ class AccountInvoiceLine(models.Model): layout_category_id = fields.Many2one('sale.layout_category', string='Section') layout_category_sequence = fields.Integer(string='Layout Sequence') # TODO: remove layout_category_sequence in master or make it work properly - price_total = fields.Monetary(compute='_compute_total_price', string='Total Amount', store=True) diff --git a/addons/sale/models/product_product.py b/addons/sale/models/product_product.py index 4844fdce..baa7b482 100644 --- a/addons/sale/models/product_product.py +++ b/addons/sale/models/product_product.py @@ -23,3 +23,6 @@ class ProductProduct(models.Model): return r sales_count = fields.Integer(compute='_sales_count', string='# Sales') + + def _get_invoice_policy(self): + return self.invoice_policy diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 4794c66b..71846a68 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -24,7 +24,7 @@ class ProductTemplate(models.Model): @api.depends('product_variant_ids.sales_count') def _sales_count(self): for product in self: - product.sales_count = sum([p.sales_count for p in product.product_variant_ids]) + product.sales_count = sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]) @api.multi def action_view_sales(self): diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 6e776639..438613c8 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -59,7 +59,7 @@ class SaleOrder(models.Model): # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in # 'account.invoice.refund') # use like as origin may contains multiple references (e.g. 'SO01, SO02') - refunds = invoice_ids.search([('origin', 'like', order.name)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund']) + refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id), ('branch_id', '=', order.branch_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund']) invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')]) # Search for refunds as well refund_ids = self.env['account.invoice'].browse() @@ -413,6 +413,9 @@ class SaleOrder(models.Model): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') invoices = {} references = {} + invoices_origin = {} + invoices_name = {} + for order in self: group_key = order.id if grouped else (order.partner_invoice_id.id, order.currency_id.id) for line in order.order_line.sorted(key=lambda l: l.qty_to_invoice < 0): @@ -423,13 +426,14 @@ class SaleOrder(models.Model): invoice = inv_obj.create(inv_data) references[invoice] = order invoices[group_key] = invoice + invoices_origin[group_key] = [invoice.origin] + invoices_name[group_key] = [invoice.name] elif group_key in invoices: - vals = {} - if order.name not in invoices[group_key].origin.split(', '): - vals['origin'] = invoices[group_key].origin + ', ' + order.name - if order.client_order_ref and order.client_order_ref not in invoices[group_key].name.split(', ') and order.client_order_ref != invoices[group_key].name: - vals['name'] = invoices[group_key].name + ', ' + order.client_order_ref - invoices[group_key].write(vals) + if order.name not in invoices_origin[group_key]: + invoices_origin[group_key].append(order.name) + if order.client_order_ref and order.client_order_ref not in invoices_name[group_key]: + invoices_name[group_key].append(order.client_order_ref) + if line.qty_to_invoice > 0: line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice) elif line.qty_to_invoice < 0 and final: @@ -437,7 +441,11 @@ class SaleOrder(models.Model): if references.get(invoices.get(group_key)): if order not in references[invoices[group_key]]: - references[invoice] = references[invoice] | order + references[invoices[group_key]] |= order + + for group_key in invoices: + invoices[group_key].write({'name': ', '.join(invoices_name[group_key]), + 'origin': ', '.join(invoices_origin[group_key])}) if not invoices: raise UserError(_('There is no invoiceable line.')) @@ -1084,23 +1092,21 @@ class SaleOrderLine(models.Model): @api.multi def name_get(self): - if self._context.get('sale_show_order_product_name'): - result = [] - for so_line in self: - name = '%s - %s' % (so_line.order_id.name, so_line.product_id.name) - result.append((so_line.id, name)) - return result - return super(SaleOrderLine, self).name_get() + result = [] + for so_line in self: + name = '%s - %s' % (so_line.order_id.name, so_line.name.split('\n')[0] or so_line.product_id.name) + if so_line.order_partner_id.ref: + name = '%s (%s)' % (name, so_line.order_partner_id.ref) + result.append((so_line.id, name)) + return result @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): - if self._context.get('sale_show_order_product_name'): - if operator in ('ilike', 'like', '=', '=like', '=ilike'): - domain = expression.AND([ - args or [], - ['|', ('order_id.name', operator, name), ('name', operator, name)] - ]) - return self.search(domain, limit=limit).name_get() + if operator in ('ilike', 'like', '=', '=like', '=ilike'): + args = expression.AND([ + args or [], + ['|', ('order_id.name', operator, name), ('name', operator, name)] + ]) return super(SaleOrderLine, self).name_search(name, args, operator, limit) @api.multi @@ -1202,7 +1208,7 @@ class SaleOrderLine(models.Model): on SO lines. This method is a hook: since analytic line are used for timesheet, expense, ... each use case should provide its part of the domain. """ - return [('so_line', 'in', self.ids), ('amount', '<=', 0.0)] + return ['&', ('so_line', 'in', self.ids), ('amount', '<=', 0.0)] @api.multi def _analytic_compute_delivered_quantity(self): @@ -1242,3 +1248,7 @@ class SaleOrderLine(models.Model): so_line.write({'qty_delivered': qty}) return True + + def _is_delivery(self): + self.ensure_one() + return False diff --git a/addons/sale/report/invoice_report_templates.xml b/addons/sale/report/invoice_report_templates.xml index 62b66b22..bf57e4d4 100644 --- a/addons/sale/report/invoice_report_templates.xml +++ b/addons/sale/report/invoice_report_templates.xml @@ -22,7 +22,7 @@ - + @@ -53,7 +53,7 @@ - diff --git a/addons/sale/report/report_all_channels_sales.py b/addons/sale/report/report_all_channels_sales.py index 2ae32b0e..60cdbf8e 100644 --- a/addons/sale/report/report_all_channels_sales.py +++ b/addons/sale/report/report_all_channels_sales.py @@ -57,6 +57,7 @@ class PosSaleReport(models.Model): (cr.date_end IS NULL OR cr.date_end > COALESCE(so.date_order, now()))) LEFT JOIN product_uom u on (u.id=sol.product_uom) LEFT JOIN product_uom u2 on (u2.id=pt.uom_id) + WHERE so.state != 'cancel' """ % self.env['res.currency']._select_companies_rates() return so_str diff --git a/addons/sale/report/report_all_channels_sales_views.xml b/addons/sale/report/report_all_channels_sales_views.xml index 623b40a5..de5744cf 100644 --- a/addons/sale/report/report_all_channels_sales_views.xml +++ b/addons/sale/report/report_all_channels_sales_views.xml @@ -4,7 +4,7 @@ report.all.channels.sales.pivotreport.all.channels.sales - + diff --git a/addons/sale/report/sale_report.xml b/addons/sale/report/sale_report.xml index 4e8a2fc7..75049cee 100644 --- a/addons/sale/report/sale_report.xml +++ b/addons/sale/report/sale_report.xml @@ -16,6 +16,7 @@ string="PRO-FORMA Invoice" model="sale.order" report_type="qweb-pdf" + groups="sale.group_proforma_sales" file="sale.report_saleorder_pro_forma" name="sale.report_saleorder_pro_forma" print_report_name="'PRO-FORMA - %s' % (object.name)" diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml index 58863bae..e41acd7d 100644 --- a/addons/sale/report/sale_report_templates.xml +++ b/addons/sale/report/sale_report_templates.xml @@ -60,6 +60,11 @@ Payment Terms:

+

+ Expiration Date: +

+

+
Branch:

diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index 533bf53d..dddba14c 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -158,9 +158,6 @@

-
-
@@ -269,7 +266,7 @@

Invoicing

-
+ +
+
+ You have credits card registered, you can log-in to be able to use them. +
+
diff --git a/addons/sale_timesheet/__init__.py b/addons/sale_timesheet/__init__.py index 81ea421d..59f04e4e 100644 --- a/addons/sale_timesheet/__init__.py +++ b/addons/sale_timesheet/__init__.py @@ -1,5 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from flectra import api, SUPERUSER_ID + from . import models from . import controllers + + +def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + env['product.template'].search([ + ('service_type', '=', 'timesheet') + ]).write({'service_type': 'manual'}) + env['product.product'].search([ + ('service_type', '=', 'timesheet') + ]).write({'service_type': 'manual'}) diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index cca87268..8507785e 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -31,4 +31,5 @@ have real delivered quantities in sales orders. 'data/sale_service_demo.xml', ], 'auto_install': True, + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py index 0c3e76d5..0df952de 100644 --- a/addons/sale_timesheet/controllers/main.py +++ b/addons/sale_timesheet/controllers/main.py @@ -56,8 +56,8 @@ class SaleTimesheetController(http.Controller): dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding) dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding) # rates - dashboard_values['rates'][billable_type] = round(data.get('unit_amount') / dashboard_total_hours * 100, 2) - dashboard_values['rates']['total'] += round(data.get('unit_amount') / dashboard_total_hours * 100, 2) + dashboard_values['rates'][billable_type] = dashboard_total_hours and round(data.get('unit_amount') / dashboard_total_hours * 100, 2) or 0 + dashboard_values['rates']['total'] += dashboard_total_hours and round(data.get('unit_amount') / dashboard_total_hours * 100, 2) or 0 # money_amount so_lines = values['timesheet_lines'].mapped('so_line') diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index 53c9a34f..37e731ee 100644 --- a/addons/sale_timesheet/models/account_invoice.py +++ b/addons/sale_timesheet/models/account_invoice.py @@ -99,7 +99,8 @@ class AccountInvoice(models.Model): line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue else: - total_revenue_per_currency[timesheet_line.company_currency_id.id] += timesheet_line.timesheet_revenue + line_revenue = timesheet_line.timesheet_revenue + total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue else: # last line: add the difference to avoid rounding problem last_price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id) total_revenue = sum([self.env['res.currency'].browse(currency_id).compute(amount, timesheet_line.company_currency_id) for currency_id, amount in total_revenue_per_currency.items()]) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index ac287286..7ba6c7c4 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -70,8 +70,9 @@ class ProjectTask(models.Model): @api.model def create(self, values): # sub task has the same so line than their parent - if 'parent_id' in values: - values['sale_line_id'] = self.env['project.task'].browse(values['parent_id']).sudo().sale_line_id.id + parent_id = values['parent_id'] if 'parent_id' in values else self.env.context.get('default_parent_id') + if parent_id: + values['sale_line_id'] = self.env['project.task'].browse(parent_id).sudo().sale_line_id.id return super(ProjectTask, self).create(values) @api.multi diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 378e0f1f..6b89f119 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -175,6 +175,7 @@ class SaleOrderLine(models.Model): @api.multi def _analytic_compute_delivered_quantity_domain(self): domain = super(SaleOrderLine, self)._analytic_compute_delivered_quantity_domain() + domain = expression.AND([domain, [('project_id', '=', False)]]) timesheet_domain = self._timesheet_compute_delivered_quantity_domain() return expression.OR([domain, timesheet_domain]) diff --git a/addons/sale_timesheet/tests/test_timesheet_revenue.py b/addons/sale_timesheet/tests/test_timesheet_revenue.py index 98d49b82..108a1e70 100644 --- a/addons/sale_timesheet/tests/test_timesheet_revenue.py +++ b/addons/sale_timesheet/tests/test_timesheet_revenue.py @@ -17,6 +17,11 @@ class TestSaleTimesheet(TestSale): # after the 6 june of current year. self.env.ref('base.rateUSDbis').unlink() + # Make sure the company is in USD + self.env.cr.execute( + "UPDATE res_company SET currency_id = %s WHERE id = %s", + [self.env.ref('base.USD').id, self.env.user.company_id.id]) + # create project self.project = self.env['project.project'].create({ 'name': 'Project for my timesheets', diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 06079078..6a16c160 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -29,7 +29,7 @@ string="Sales Order"/> - +
Unit Price Disc.(%) TaxesPriceAmount
+