diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index d2f7de3be52..70e6e44d719 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -97,7 +97,7 @@ "default": "1", "fieldname": "unlink_advance_payment_on_cancelation_of_order", "fieldtype": "Check", - "label": "Unlink Advance Payment on Cancelation of Order" + "label": "Unlink Advance Payment on Cancellation of Order" }, { "default": "1", @@ -179,7 +179,7 @@ "icon": "icon-cog", "idx": 1, "issingle": 1, - "modified": "2020-03-11 13:09:26.235848", + "modified": "2020-10-08 09:40:12.121145", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index cf1ad6eab6f..077a11a9be6 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -138,7 +138,7 @@ class GLEntry(Document): frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) - if self.cost_center and _check_is_group(): + if not self.flags.from_repost and self.cost_center and _check_is_group(): frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 09feb7d97bc..27541670a89 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -31,13 +31,19 @@ apply_on_table = { } def get_pricing_rules(args, doc=None): - pricing_rules = [] - values = {} + pricing_rules_all = [] + values = {} for apply_on in ['Item Code', 'Item Group', 'Brand']: - pricing_rules.extend(_get_pricing_rules(apply_on, args, values)) - if pricing_rules and not apply_multiple_pricing_rules(pricing_rules): - break + pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values)) + + # removing duplicate pricing rule + pricing_rules_name = [] + pricing_rules = [] + for p in pricing_rules_all: + if p['name'] not in pricing_rules_name: + pricing_rules_name.append(p['name']) + pricing_rules.append(p) rules = [] @@ -323,9 +329,10 @@ def apply_internal_priority(pricing_rules, field_set, args): filtered_rules = [] for field in field_set: if args.get(field): - # filter function always returns a filter object even if empty - # list conversion is necessary to check for an empty result - filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules)) + for rule in pricing_rules: + if rule.get(field) == args.get(field): + filtered_rules = [rule] + break if filtered_rules: break return filtered_rules or pricing_rules diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c9fd889b63f..155bfd4416a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -636,7 +636,8 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount / self.conversion_rate) }, item=item)) else: - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", + asset_category=item.asset_category,company=self.company) cwip_account_currency = get_account_currency(cwip_account) gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4019815e19a..329433ebc10 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -939,7 +939,8 @@ def make_purchase_invoice(**args): "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "" + "rejected_serial_no": args.rejected_serial_no or "", + "asset_location": args.location or "" }) if args.get_taxes_and_charges: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index b35e32c5ca5..4be4c269f17 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,8 +13,7 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'], - 'Delivery Note': ['items', 'delivery_note'] + 'Sales Order': ['items', 'sales_order'] }, 'transactions': [ { diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index bbbcc7f3a69..632e30db45d 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template +from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity +from erpnext.crm.doctype.opportunity.opportunity import make_quotation test_records = frappe.get_test_records('Tax Rule') @@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase): self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), "_Test Sales Taxes and Charges Template 1 - _TC") + def test_taxes_fetch_via_tax_rule(self): + make_tax_rule(customer= "_Test Customer", billing_city = "_Test City", + sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + + # create opportunity for customer + opportunity = make_opportunity(with_items=1) + + # make quotation from opportunity + quotation = make_quotation(opportunity.name) + quotation.save() + + self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC") + + # Check if accounts heads and rate fetched are also fetched from tax template or not + self.assertTrue(len(quotation.taxes) > 0) + + def make_tax_rule(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b539fff74e9..f6a7218d601 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai from `tabGL Entry` where company = %s and party in %s and fiscal_year=%s and credit > 0 + and is_opening = 'No' """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] @@ -139,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai else: tds_amount = _get_tds(net_total, tax_details.rate) else: - supplier_credit_amount = frappe.get_all('Purchase Invoice Item', - fields = ['sum(net_amount)'], - filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1) + supplier_credit_amount = frappe.get_all('Purchase Invoice', + fields = ['sum(net_total)'], + filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) supplier_credit_amount = (supplier_credit_amount[0][0] if supplier_credit_amount and supplier_credit_amount[0][0] else 0) @@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No select distinct voucher_no from `tabGL Entry` where party in %s and %s and debit > 0 + and is_opening = 'No' """, (tuple(suppliers), condition)) or [] def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index b1468999fc1..a0b0cbb9956 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + # TDS not applied + pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True) + pi.submit() + invoices.append(pi) + + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + self.assertEqual(pi.taxes_and_charges_deducted, 2000) + self.assertEqual(pi.grand_total, 8000) + + # delete invoices to avoid clashing + for d in invoices: + d.cancel() + def create_purchase_invoice(**args): # return sales invoice doc object item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) @@ -109,7 +132,7 @@ def create_purchase_invoice(**args): pi = frappe.get_doc({ "doctype": "Purchase Invoice", "posting_date": today(), - "apply_tds": 1, + "apply_tds": 0 if args.do_not_apply_tds else 1, "supplier": args.supplier, "company": '_Test Company', "taxes_and_charges": "", diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index cecf7f5e6fc..1e82e54cdf8 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $(frappe.render_template("pos_item", { item_code: escape(obj.name), item_price: item_price, - title: obj.name || obj.item_name, + title: obj.name === obj.item_name ? obj.name : obj.item_name, item_name: obj.name === obj.item_name ? "" : obj.item_name, item_image: obj.image, item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj), @@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $.each(this.frm.doc.items || [], function (i, d) { $(frappe.render_template("pos_bill_item_new", { item_code: escape(d.item_code), - title: d.item_code || d.item_name, + title: d.item_code === d.item_name ? d.item_code : d.item_name, item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name), qty: d.qty, discount_percentage: d.discount_percentage || 0.0, diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 50947ecf5ef..2bb09cf0dc5 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -44,7 +44,7 @@ - {% for(let j=0, k=data.length-1; j=0 - AND par.name=child.parent - AND par.docstatus=1 + parent.per_ordered>=0 + AND parent.name=child.parent + AND parent.docstatus=1 {conditions} """.format(conditions=conditions), as_dict=1) #nosec @@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions): status=record.status, actual_cost=0, purchase_order_amt=0, - purchase_order_amt_in_company_currency=0 + purchase_order_amt_in_company_currency=0, + project = record.project, + cost_center = record.cost_center ) procurement_record_against_mr.append(procurement_record_details) return mr_records, procurement_record_against_mr @@ -280,16 +284,16 @@ def get_po_entries(conditions): child.amount, child.base_amount, child.schedule_date, - par.transaction_date, - par.supplier, - par.status, - par.owner - FROM `tabPurchase Order` par, `tabPurchase Order Item` child + parent.transaction_date, + parent.supplier, + parent.status, + parent.owner + FROM `tabPurchase Order` parent, `tabPurchase Order Item` child WHERE - par.docstatus = 1 - AND par.name = child.parent - AND par.status not in ("Closed","Completed","Cancelled") + parent.docstatus = 1 + AND parent.name = child.parent + AND parent.status not in ("Closed","Completed","Cancelled") {conditions} GROUP BY - par.name, child.item_code + parent.name, child.item_code """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bc90011bf12..12b8f697cde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1209,7 +1209,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil try: doc.check_permission(perm_type) except frappe.PermissionError: - actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + actions = { 'create': 'add', 'write': 'update'} frappe.throw(_("You do not have permissions to {} items in a {}.") .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) @@ -1252,7 +1252,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - check_doc_permissions(parent, 'cancel') + check_doc_permissions(parent, 'write') validate_and_delete_children(parent, data) for d in data: @@ -1284,19 +1284,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil validate_quantity(child_item, d) child_item.qty = flt(d.get("qty")) - precision = child_item.precision("rate") or 2 + rate_precision = child_item.precision("rate") or 2 + conv_fac_precision = child_item.precision("conversion_factor") or 2 + qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision): + if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision): frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") .format(child_item.idx, child_item.item_code)) else: - child_item.rate = flt(d.get("rate")) + child_item.rate = flt(d.get("rate"), rate_precision) if d.get("conversion_factor"): if child_item.stock_uom == child_item.uom: child_item.conversion_factor = 1 else: - child_item.conversion_factor = flt(d.get('conversion_factor')) + child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) if d.get("delivery_date") and parent_doctype == 'Sales Order': child_item.delivery_date = d.get('delivery_date') diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 1399654ffd2..3ebb12541ab 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -10,6 +10,7 @@ from erpnext.stock.utils import get_incoming_rate from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.item.item import set_item_default from frappe.contacts.doctype.address.address import get_address_display +from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.stock_controller import StockController @@ -53,10 +54,10 @@ class SellingController(StockController): super(SellingController, self).set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned - self.set_missing_lead_customer_details() + self.set_missing_lead_customer_details(for_validate=for_validate) self.set_price_list_and_item_details(for_validate=for_validate) - def set_missing_lead_customer_details(self): + def set_missing_lead_customer_details(self, for_validate=False): customer, lead = None, None if getattr(self, "customer", None): customer = self.customer @@ -93,6 +94,11 @@ class SellingController(StockController): posting_date=self.get('transaction_date') or self.get('posting_date'), company=self.company)) + if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate: + taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges) + for tax in taxes: + self.append('taxes', tax) + def set_price_list_and_item_details(self, for_validate=False): self.set_price_list_currency("Selling") self.set_missing_item_details(for_validate=for_validate) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index c8f42a5921f..28bfb7a0072 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -629,22 +629,29 @@ class calculate_taxes_and_totals(object): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) def update_paid_amount_for_return(self, total_amount_to_pay): - default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', - {'parent': self.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'type', 'account'], as_dict=1) + existing_amount = 0 - self.doc.payments = [] + for payment in self.doc.payments: + existing_amount += payment.amount - if default_mode_of_payment: - self.doc.append('payments', { - 'mode_of_payment': default_mode_of_payment.mode_of_payment, - 'type': default_mode_of_payment.type, - 'account': default_mode_of_payment.account, - 'amount': total_amount_to_pay - }) - else: - self.doc.is_pos = 0 - self.doc.pos_profile = '' + # do not override user entered amount if equal to total_amount_to_pay + if existing_amount != total_amount_to_pay: + default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', + {'parent': self.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'type', 'account'], as_dict=1) + + self.doc.payments = [] + + if default_mode_of_payment: + self.doc.append('payments', { + 'mode_of_payment': default_mode_of_payment.mode_of_payment, + 'type': default_mode_of_payment.type, + 'account': default_mode_of_payment.account, + 'amount': total_amount_to_pay + }) + else: + self.doc.is_pos = 0 + self.doc.pos_profile = '' self.calculate_paid_amount() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c67330ad45f..2bf883809da 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -251,6 +251,10 @@ def make_bom(**args): 'rate': item_doc.valuation_rate or args.rate, }) - bom.insert(ignore_permissions=True) - bom.submit() + if not args.do_not_save: + bom.insert(ignore_permissions=True) + + if not args.do_not_submit: + bom.submit() + return bom diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f917b098688..d82a4dd9fe8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -371,6 +371,49 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) + def test_operation_time_with_batch_size(self): + fg_item = "Test Batch Size Item For BOM" + rm1 = "Test Batch Size Item RM 1 For BOM" + + for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]: + make_item(item, { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + }) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.with_operations = 1 + bom.append("operations", { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + "batch_size": 5 + }) + + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, + planned_start_date=now(), qty=1, do_not_save=True) + + work_order.set_work_order_operations() + work_order.save() + self.assertEqual(work_order.operations[0].time_in_mins, 8.0) + + work_order1 = make_wo_order_test_record(item=fg_item, + planned_start_date=now(), qty=5, do_not_save=True) + + work_order1.set_work_order_operations() + work_order1.save() + self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d14c8d82f11..f9c028563bb 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -611,7 +611,7 @@ erpnext.work_order = { description: __('Max: {0}', [max]), default: max }, data => { - max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; if (data.qty > max) { frappe.msgprint(__('Quantity must not be more than {0}', [max])); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index b0585e5d734..603c8d4928c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -364,7 +364,7 @@ class WorkOrder(Document): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 60aa3b64e82..efad078fe37 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -375,7 +375,7 @@ def get_items(filters=None, search=None): results = frappe.db.sql(''' SELECT - `tabItem`.`name`, `tabItem`.`item_name`, + `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`, `tabItem`.`website_image`, `tabItem`.`image`, `tabItem`.`web_long_description`, `tabItem`.`description`, `tabItem`.`route` diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1e99aa3bf0b..8281bd98e4a 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -594,7 +594,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, @@ -672,25 +672,33 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ ); } - frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'account', 'type'], (value) => { - if (this.frm.is_dirty()) { - frappe.model.clear_table(this.frm.doc, 'payments'); - if (value) { - let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); - row.mode_of_payment = value.mode_of_payment; - row.type = value.type; - row.account = value.account; - row.default = 1; - row.amount = total_amount_to_pay; - } else { - this.frm.set_value('is_pos', 1); - } - this.frm.refresh_fields(); - } - }, 'Sales Invoice'); + let existing_amount = 0 + $.each(this.frm.doc.payments || [], function(i, row) { + existing_amount += row.amount; + }) - this.calculate_paid_amount(); + if (existing_amount != total_amount_to_pay) { + frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'account', 'type'], (value) => { + if (this.frm.is_dirty()) { + frappe.model.clear_table(this.frm.doc, 'payments'); + if (value) { + let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); + row.mode_of_payment = value.mode_of_payment; + row.type = value.type; + row.account = value.account; + row.default = 1; + row.amount = total_amount_to_pay; + } else { + this.frm.set_value('is_pos', 1); + } + this.frm.refresh_fields(); + this.calculate_paid_amount(); + } + }, 'Sales Invoice'); + } else { + this.calculate_paid_amount(); + } }, set_default_payment: function(total_amount_to_pay, update_paid_amount) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9b1c94e5ba0..264344ca94c 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) { const frm = opts.frm; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; + const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); + const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; + this.data = []; const fields = [{ fieldtype:'Data', @@ -472,14 +475,16 @@ erpnext.utils.update_child_items = function(opts) { default: 0, read_only: 0, in_list_view: 1, - label: __('Qty') + label: __('Qty'), + precision: get_precision("qty") }, { fieldtype:'Currency', fieldname:"rate", default: 0, read_only: 0, in_list_view: 1, - label: __('Rate') + label: __('Rate'), + precision: get_precision("rate") }]; if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { @@ -494,7 +499,8 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: 'Float', fieldname: "conversion_factor", in_list_view: 1, - label: __("Conversion Factor") + label: __("Conversion Factor"), + precision: get_precision('conversion_factor') }) } diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index cf2644e0053..ac876229ecb 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', { } }; }); + + frm.set_query('parent_quality_procedure', function(){ + return { + filters: { + is_group: 1 + } + }; + }); } }); \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index b3c0d948909..32a1ebcc0ca 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -21,8 +21,7 @@ "fieldname": "parent_quality_procedure", "fieldtype": "Link", "label": "Parent Procedure", - "options": "Quality Procedure", - "read_only": 1 + "options": "Quality Procedure" }, { "default": "0", @@ -73,7 +72,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-06-17 17:25:03.434953", + "modified": "2020-10-12 16:14:11.167537", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 1952e578673..797c26b64c2 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, rebuild_tree from frappe import _ class QualityProcedure(NestedSet): @@ -42,6 +42,8 @@ class QualityProcedure(NestedSet): doc.save(ignore_permissions=True) def set_parent(self): + rebuild_tree('Quality Procedure', 'parent_quality_procedure') + for process in self.processes: # Set parent for only those children who don't have a parent parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 1484f6b2290..a4a63d2aef2 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -129,7 +129,7 @@ class Customer(TransactionBase): address = frappe.get_doc('Address', address_name.get('name')) if not address.has_link('Customer', self.name): address.append('links', dict(link_doctype='Customer', link_name=self.name)) - address.save() + address.save(ignore_permissions=self.flags.ignore_permissions) lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) @@ -147,7 +147,7 @@ class Customer(TransactionBase): contact = frappe.get_doc('Contact', contact_name.get('name')) if not contact.has_link('Customer', self.name): contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save() + contact.save(ignore_permissions=self.flags.ignore_permissions) else: lead.lead_name = lead.lead_name.lstrip().split(" ") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index ee6b429ccae..dfb284b7682 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -108,6 +108,10 @@ class TestQuotation(unittest.TestCase): sales_order.transaction_date = nowdate() sales_order.insert() + # Remove any unknown taxes if applied + sales_order.set('taxes', []) + sales_order.save() + self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date)) self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index b4e151b2e30..423922e4865 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -169,7 +169,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } // project - if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { + if(flt(doc.per_delivered, 2) < 100) { this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create')); } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 31c328fdf3e..55458a51e98 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -88,6 +88,8 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(len(si.get("items")), 1) si.insert() + si.set('taxes', []) + si.save() self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date) @@ -400,6 +402,22 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + + def test_update_child_with_precision(self): + from frappe.model.meta import get_field_precision + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) + + make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") + so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) + update_child_qty_rate('Sales Order', trans_item, so.name) + + so.reload() + self.assertEqual(so.items[0].rate, 200.34669) + make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_qty_rate_perm(self): so = make_sales_order(item_code= "_Test Item", qty=4) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 335cad3598b..60dacb54d1a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -372,7 +372,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue='long', company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index b2cffbbf0d8..abff97364c0 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cstr +from frappe.utils import cstr, cint from frappe import msgprint, throw, _ from frappe.model.document import Document @@ -159,7 +159,7 @@ class NamingSeries(Document): prefix = self.parse_naming_series() self.insert_series(prefix) frappe.db.sql("update `tabSeries` set current = %s where name = %s", - (self.current_value, prefix)) + (cint(self.current_value), prefix)) msgprint(_("Series Updated Successfully")) else: msgprint(_("Please select prefix first")) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index deace33f343..e6634d29fe1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -111,6 +111,7 @@ class Item(WebsiteGenerator): self.synced_with_hub = 0 self.validate_has_variants() + self.validate_attributes_in_variants() self.validate_stock_exists_for_template_item() self.validate_attributes() self.validate_variant_attributes() @@ -806,6 +807,76 @@ class Item(WebsiteGenerator): if frappe.db.exists("Item", {"variant_of": self.name}): frappe.throw(_("Item has variants.")) + def validate_attributes_in_variants(self): + if not self.has_variants or self.get("__islocal"): + return + + old_doc = self.get_doc_before_save() + old_doc_attributes = set([attr.attribute for attr in old_doc.attributes]) + own_attributes = [attr.attribute for attr in self.attributes] + + # Check if old attributes were removed from the list + # Is old_attrs is a subset of new ones + # that means we need not check any changes + if old_doc_attributes.issubset(set(own_attributes)): + return + + from collections import defaultdict + + # get all item variants + items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})] + + # get all deleted attributes + deleted_attribute = list(old_doc_attributes.difference(set(own_attributes))) + + # fetch all attributes of these items + item_attributes = frappe.get_all( + "Item Variant Attribute", + filters={ + "parent": ["in", items], + "attribute": ["in", deleted_attribute] + }, + fields=["attribute", "parent"] + ) + not_included = defaultdict(list) + + for attr in item_attributes: + if attr["attribute"] not in own_attributes: + not_included[attr["parent"]].append(attr["attribute"]) + + if not len(not_included): + return + + def body(docnames): + docnames.sort() + return "
".join(docnames) + + def table_row(title, body): + return """ + {0} + {1} + """.format(title, body) + + rows = '' + for docname, attr_list in not_included.items(): + link = "{0}".format(frappe.bold(_(docname))) + rows += table_row(link, body(attr_list)) + + error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + + message = """ +
{0}

+ + + + + + {3} +
{1}{2}
+ """.format(error_description, _('Variant Items'), _('Attributes'), rows) + + frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True) + def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) @@ -1001,8 +1072,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by pr.posting_date desc, pr.posting_time desc, pr.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - - + + purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01") purchase_receipt_date = getdate(last_purchase_receipt and @@ -1010,7 +1081,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): # use purchase order - + last_purchase = last_purchase_order[0] purchase_date = purchase_order_date @@ -1030,7 +1101,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): "discount_percentage": flt(last_purchase.discount_percentage), "purchase_date": purchase_date }) - + conversion_rate = flt(conversion_rate) or 1.0 out.update({ @@ -1069,8 +1140,7 @@ def invalidate_item_variants_cache_for_website(doc): if item_code: item_cache = ItemVariantsCacheManager(item_code) - item_cache.clear_cache() - + item_cache.rebuild_cache() def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.json b/erpnext/stock/doctype/item_attribute/item_attribute.json index 2fbff4e614e..5c4678916f3 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.json +++ b/erpnext/stock/doctype/item_attribute/item_attribute.json @@ -1,357 +1,97 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:attribute_name", - "beta": 0, - "creation": "2014-09-26 03:49:54.899170", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:attribute_name", + "creation": "2014-09-26 03:49:54.899170", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "attribute_name", + "numeric_values", + "section_break_4", + "from_range", + "increment", + "column_break_8", + "to_range", + "section_break_5", + "item_attribute_values" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attribute_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "attribute_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Name", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "numeric_values", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Numeric Values", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "numeric_values", - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "numeric_values", + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "from_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "from_range", + "fieldtype": "Float", + "label": "From Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "increment", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Increment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "increment", + "fieldtype": "Float", + "label": "Increment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "to_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "to_range", + "fieldtype": "Float", + "label": "To Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: !doc.numeric_values", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval: !doc.numeric_values", + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "item_attribute_values", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Attribute Values", - "length": 0, - "no_copy": 0, - "options": "Item Attribute Value", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "item_attribute_values", + "fieldtype": "Table", + "label": "Item Attribute Values", + "options": "Item Attribute Value" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-edit", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-01 13:17:46.524806", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Attribute", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-edit", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-02 12:03:02.359202", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Attribute", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Item Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 2f75bbd97c0..7f00201587a 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ +from frappe.utils import flt from erpnext.controllers.item_variant import (validate_is_incremental, validate_item_attribute_value, InvalidItemAttributeValueError) @@ -42,7 +43,7 @@ class ItemAttribute(Document): if self.from_range is None or self.to_range is None: frappe.throw(_("Please specify from/to range")) - elif self.from_range >= self.to_range: + elif flt(self.from_range) >= flt(self.to_range): frappe.throw(_("From Range has to be less than To Range")) if not self.increment: diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 8e39eb5037d..51b47c50a3b 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -50,16 +50,18 @@ class ItemPrice(Document): def check_duplicates(self): conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" + condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name) for field in ['uom', 'valid_from', 'valid_upto', 'packing_unit', 'customer', 'supplier']: if self.get(field): conditions += " and {0} = %({1})s".format(field, field) + condition_data_dict[field] = self.get(field) price_list_rate = frappe.db.sql(""" SELECT price_list_rate FROM `tabItem Price` - {conditions} """.format(conditions=conditions), self.as_dict()) + {conditions} """.format(conditions=conditions), condition_data_dict) if price_list_rate : frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 42cc473b18d..08553675f4c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -207,6 +207,7 @@ class PurchaseReceipt(BuyingController): from erpnext.accounts.general_ledger import process_gl_map stock_rbnb = self.get_company_default("stock_received_but_not_billed") + stock_rbnb_currency = get_account_currency(stock_rbnb) cogs_account = self.get_company_default("default_expense_account") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -243,7 +244,6 @@ class PurchaseReceipt(BuyingController): # stock received but not billed if d.base_net_amount: - stock_rbnb_currency = get_account_currency(stock_rbnb) gl_entries.append(self.get_gl_dict({ "account": stock_rbnb, "against": warehouse_account[d.warehouse]["account"], @@ -289,6 +289,7 @@ class PurchaseReceipt(BuyingController): if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: + cogs_account = self.get_company_default("default_expense_account") loss_account = cogs_account gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ca59e67a676..003403a2f8a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -172,8 +172,9 @@ class StockReconciliation(StockController): row.serial_no = '' # item managed batch-wise not allowed - if item.has_batch_no and not row.batch_no and not item.create_new_batch: - raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) + if item.has_batch_no and not row.batch_no and not frappe.flags.in_test: + if not item.create_new_batch or self.purpose != 'Opening Stock': + raise frappe.ValidationError(_("Batch no is required for the batched item {0}").format(item_code)) # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) @@ -191,10 +192,11 @@ class StockReconciliation(StockController): serialized_items = False for row in self.items: item = frappe.get_cached_doc("Item", row.item_code) - if not (item.has_serial_no or item.has_batch_no): - if row.serial_no or row.batch_no: + if not (item.has_serial_no): + if row.serial_no: frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ .format(row.idx, frappe.bold(row.item_code))) + previous_sle = get_previous_sle({ "item_code": row.item_code, "warehouse": row.warehouse, @@ -217,7 +219,12 @@ class StockReconciliation(StockController): or (not previous_sle and not row.qty)): continue - sl_entries.append(self.get_sle_for_items(row)) + sle_data = self.get_sle_for_items(row) + + if row.batch_no: + sle_data.actual_qty = row.quantity_difference + + sl_entries.append(sle_data) else: serialized_items = True @@ -244,7 +251,7 @@ class StockReconciliation(StockController): serial_nos = get_serial_nos(row.serial_no) or [] # To issue existing serial nos - if row.current_qty and (row.current_serial_no or row.batch_no): + if row.current_qty and (row.current_serial_no): args = self.get_sle_for_items(row) args.update({ 'actual_qty': -1 * row.current_qty, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index a679c9415d0..8b073ec5ab4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -361,6 +361,37 @@ class TestStockReconciliation(unittest.TestCase): doc.cancel() frappe.delete_doc(doc.doctype, doc.name) + def test_allow_negative_for_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + item_code = "Stock-Reco-batch-Item-5" + warehouse = "_Test Warehouse for Stock Reco5 - _TC" + + create_warehouse("_Test Warehouse for Stock Reco5", {"is_group": 0, + "parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"}) + + batch_item_doc = create_item(item_code, is_stock_item=1) + if not batch_item_doc.has_batch_no: + frappe.db.set_value("Item", item_code, { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "Test-C.####" + }) + + ste1=make_stock_entry(posting_date="2020-10-07", posting_time="02:00", item_code=item_code, + target=warehouse, qty=2, basic_rate=100) + + batch_no = ste1.items[0].batch_no + + ste2=make_stock_entry(posting_date="2020-10-09", posting_time="02:00", item_code=item_code, + source=warehouse, qty=2, basic_rate=100, batch_no=batch_no) + + sr = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, batch_no=batch_no, rate=200) + + for doc in [sr, ste2, ste1]: + doc.cancel() + frappe.delete_doc(doc.doctype, doc.name) + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 84e95e27ca0..4204aee342b 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -3,6 +3,14 @@ frappe.query_reports["Batch-Wise Balance History"] = { "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, { "fieldname":"from_date", "label": __("From Date"), @@ -18,6 +26,47 @@ frappe.query_reports["Batch-Wise Balance History"] = { "width": "80", "default": frappe.datetime.get_today(), "reqd": 1 - } + }, + { + "fieldname":"item_code", + "label": __("Item Code"), + "fieldtype": "Link", + "options": "Item", + "get_query": function() { + return { + filters: { + "has_batch_no": 1 + } + } + } + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + let company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "company": company + } + } + } + }, + { + "fieldname":"batch_no", + "label": __("Batch No"), + "fieldtype": "Link", + "options": "Batch", + "get_query": function() { + let item_code = frappe.query_report.get_filter_value('item_code'); + return { + filters: { + "item": item_code + } + } + } + }, ] } \ No newline at end of file diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index ec2ef35bb41..1999b7404e6 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -53,6 +53,10 @@ def get_conditions(filters): else: frappe.throw(_("'To Date' is required")) + for field in ["item_code", "warehouse", "batch_no", "company"]: + if filters.get(field): + conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field))) + return conditions #get all details diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index aa3ed92079c..12f32972039 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -77,38 +77,33 @@ def get_price_list(): return item_rate_map def get_last_purchase_rate(): - item_last_purchase_rate_map = {} - query = """select * from (select - result.item_code, - result.base_rate - from ( - (select - po_item.item_code, - po_item.item_name, - po.transaction_date as posting_date, - po_item.base_price_list_rate, - po_item.discount_percentage, - po_item.base_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.name = po_item.parent and po.docstatus = 1) - union - (select - pr_item.item_code, - pr_item.item_name, - pr.posting_date, - pr_item.base_price_list_rate, - pr_item.discount_percentage, - pr_item.base_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.name = pr_item.parent and pr.docstatus = 1) - ) result - order by result.item_code asc, result.posting_date desc) result_wrapper - group by item_code""" + query = """select * from ( + (select + po_item.item_code, + po.transaction_date as posting_date, + po_item.base_rate + from `tabPurchase Order` po, `tabPurchase Order Item` po_item + where po.name = po_item.parent and po.docstatus = 1) + union + (select + pr_item.item_code, + pr.posting_date, + pr_item.base_rate + from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item + where pr.name = pr_item.parent and pr.docstatus = 1) + union + (select + pi_item.item_code, + pi.posting_date, + pi_item.base_rate + from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item + where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1) + ) result order by result.item_code asc, result.posting_date asc""" for d in frappe.db.sql(query, as_dict=1): - item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate) + item_last_purchase_rate_map[d.item_code] = d.base_rate return item_last_purchase_rate_map diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 99d816c4a24..953939bccb8 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -17,14 +17,17 @@ def execute(filters=None): data = [] for item, item_dict in iteritems(item_details): + earliest_age, latest_age = 0, 0 fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) details = item_dict["details"] - if not fifo_queue or (not item_dict.get("total_qty")): continue + if not fifo_queue and (not item_dict.get("total_qty")): continue average_age = get_average_age(fifo_queue, to_date) - earliest_age = date_diff(to_date, fifo_queue[0][1]) - latest_age = date_diff(to_date, fifo_queue[-1][1]) + + if fifo_queue: + earliest_age = date_diff(to_date, fifo_queue[0][1]) + latest_age = date_diff(to_date, fifo_queue[-1][1]) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -147,7 +150,8 @@ def get_fifo_queue(filters, sle=None): item_details.setdefault(key, {"details": d, "fifo_queue": []}) fifo_queue = item_details[key]["fifo_queue"] - transferred_item_details.setdefault((d.voucher_no, d.name), []) + transferred_item_key = (d.voucher_no, d.name, d.warehouse) + transferred_item_details.setdefault(transferred_item_key, []) if d.voucher_type == "Stock Reconciliation": d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0)) @@ -155,10 +159,10 @@ def get_fifo_queue(filters, sle=None): serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else [] if d.actual_qty > 0: - if transferred_item_details.get((d.voucher_no, d.name)): - batch = transferred_item_details[(d.voucher_no, d.name)][0] + if transferred_item_details.get(transferred_item_key): + batch = transferred_item_details[transferred_item_key][0] fifo_queue.append(batch) - transferred_item_details[((d.voucher_no, d.name))].pop(0) + transferred_item_details[transferred_item_key].pop(0) else: if serial_no_list: for serial_no in serial_no_list: @@ -182,11 +186,11 @@ def get_fifo_queue(filters, sle=None): # if batch qty > 0 # not enough or exactly same qty in current batch, clear batch qty_to_pop -= flt(batch[0]) - transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) + transferred_item_details[transferred_item_key].append(fifo_queue.pop(0)) else: # all from current batch batch[0] = flt(batch[0]) - qty_to_pop - transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) + transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]]) qty_to_pop = 0 item_details[key]["qty_after_transaction"] = d.qty_after_transaction diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 6a265ec4cc5..67d3f233c81 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.utils import cint, flt from erpnext.stock.utils import update_included_uom_in_report +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos def execute(filters=None): include_uom = filters.get("include_uom") @@ -23,6 +24,7 @@ def execute(filters=None): actual_qty = stock_value = 0 + available_serial_nos = {} for sle in sl_entries: item_detail = item_details[sle.item_code] @@ -41,6 +43,9 @@ def execute(filters=None): "stock_value": stock_value }) + if sle.serial_no: + update_available_serial_nos(available_serial_nos, sle) + data.append(sle) if include_uom: @@ -49,6 +54,27 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data +def update_available_serial_nos(available_serial_nos, sle): + serial_nos = get_serial_nos(sle.serial_no) + key = (sle.item_code, sle.warehouse) + if key not in available_serial_nos: + available_serial_nos.setdefault(key, []) + + existing_serial_no = available_serial_nos[key] + for sn in serial_nos: + if sle.actual_qty > 0: + if sn in existing_serial_no: + existing_serial_no.remove(sn) + else: + existing_serial_no.append(sn) + else: + if sn in existing_serial_no: + existing_serial_no.remove(sn) + else: + existing_serial_no.append(sn) + + sle.balance_serial_no = '\n'.join(existing_serial_no) + def get_columns(): columns = [ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95}, @@ -70,7 +96,8 @@ def get_columns(): {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial #"), "fieldname": "serial_no", "width": 100}, + {"label": _("Serial No"), "fieldname": "serial_no", "width": 100}, + {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} ] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5c4bba730e3..4fa080a2fd2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -162,10 +162,13 @@ class update_entries_after(object): self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) else: - if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: - # assert + if sle.voucher_type=="Stock Reconciliation": + if sle.batch_no: + self.qty_after_transaction += flt(sle.actual_qty) + else: + self.qty_after_transaction = sle.qty_after_transaction + self.valuation_rate = sle.valuation_rate - self.qty_after_transaction = sle.qty_after_transaction self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]] self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) else: