diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 113bea00645..533eda31d58 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -254,7 +254,8 @@ def create_account(**kwargs): account_name = kwargs.get('account_name'), account_type = kwargs.get('account_type'), parent_account = kwargs.get('parent_account'), - company = kwargs.get('company') + company = kwargs.get('company'), + account_currency = kwargs.get('account_currency') )) account.save() diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 41f9ce030a1..a3c29b6d640 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -21,6 +21,7 @@ "book_asset_depreciation_entry_automatically", "add_taxes_from_item_tax_template", "automatically_fetch_payment_terms", + "delete_linked_ledger_entries", "deferred_accounting_settings_section", "automatically_process_deferred_accounting_entry", "book_deferred_entries_based_on", @@ -219,6 +220,12 @@ "fieldtype": "Select", "label": "Book Deferred Entries Based On", "options": "Days\nMonths" + }, + { + "default": "0", + "fieldname": "delete_linked_ledger_entries", + "fieldtype": "Check", + "label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction" } ], "icon": "icon-cog", @@ -226,7 +233,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:32:52.268826", + "modified": "2021-01-05 13:04:00.118892", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -254,4 +261,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index a749f0e15cd..b0a864f76cd 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -159,8 +159,8 @@ class GLEntry(Document): if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ 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))) + 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))) def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) @@ -170,7 +170,7 @@ class GLEntry(Document): account_currency = get_account_currency(self.account) if not self.account_currency: - self.account_currency = company_currency + self.account_currency = account_currency or company_currency if account_currency != self.account_currency: frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4a952a30a26..06aa20bfc5a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -275,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ supplier: function() { var me = this; - if(this.frm.updating_party_details) + + // Do not update if inter company reference is there as the details will already be updated + if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index c64ffd878c4..451c9368816 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -57,8 +57,8 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", - "supplier_warehouse", "items_section", "update_stock", "scan_barcode", @@ -515,6 +515,7 @@ }, { "depends_on": "update_stock", + "description": "Sets 'Accepted Warehouse' in each row of the items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Accepted Warehouse", @@ -543,17 +544,6 @@ "options": "No\nYes", "print_hide": 1 }, - { - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "label": "Supplier Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1, - "print_width": "50px", - "width": "50px" - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1232,7 +1222,9 @@ "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", "label": "Inter Company Invoice Reference", + "no_copy": 1, "options": "Sales Invoice", + "print_hide": 1, "read_only": 1 }, { @@ -1356,13 +1348,25 @@ "fieldtype": "Link", "label": "Represents Company", "options": "Company" + }, + { + "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:46:12.796378", + "modified": "2020-12-26 20:49:03.305063", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b52678e8d3b..dacd50a3e24 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -443,7 +443,7 @@ class PurchaseInvoice(BuyingController): else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None - + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -457,7 +457,7 @@ class PurchaseInvoice(BuyingController): self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) - + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController): grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) gl_entries.append( @@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): - voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") if d.category in ('Valuation', 'Total and Valuation') @@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController): ) else: - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # Amount added through landed-cost-voucher if landed_cost_entries: @@ -582,7 +583,8 @@ class PurchaseInvoice(BuyingController): "against": item.expense_account, "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": item.project or self.project }, item=item)) @@ -624,13 +626,14 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: expense_account = service_received_but_not_billed_account - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": amount, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item)) + if not self.is_internal_transfer(): + gl_entries.append(self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": amount, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item)) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: @@ -795,10 +798,10 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)): + warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): cost_of_goods_sold_account = self.get_company_default("default_expense_account") - stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision) + stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( @@ -999,10 +1002,10 @@ class PurchaseInvoice(BuyingController): self.delete_auto_created_batches() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + self.update_project() frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index f6d76e50502..1f7853dbf71 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -87,6 +88,7 @@ "po_detail", "purchase_receipt", "pr_detail", + "sales_invoice_item", "item_weight_details", "weight_per_unit", "total_weight", @@ -553,8 +555,8 @@ "fieldtype": "Link", "hidden": 1, "label": "Brand", - "print_hide": 1, - "options": "Brand" + "options": "Brand", + "print_hide": 1 }, { "fetch_from": "item_code.item_group", @@ -562,9 +564,9 @@ "fieldname": "item_group", "fieldtype": "Link", "label": "Item Group", + "options": "Item Group", "print_hide": 1, - "read_only": 1, - "options": "Item Group" + "read_only": 1 }, { "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", @@ -759,10 +761,11 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier && parent.update_stock", "fieldname": "from_warehouse", "fieldtype": "Link", "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -779,11 +782,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-08-20 11:48:01.398356", + "links": [], + "modified": "2020-12-26 17:20:36.415791", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -791,4 +803,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 72199a92aa2..f2a62cdacd1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -130,16 +130,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.set_default_print_format(); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { - frappe.model.with_doc("Customer", me.frm.doc.customer, function() { - var customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - var internal = customer.is_internal_customer; - var disabled = customer.disabled; - if (internal == 1 && disabled == 0) { - me.frm.add_custom_button("Inter Company Invoice", function() { - me.make_inter_company_invoice(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" : + "Inter Company Purchase Invoice"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_invoice(); + }, __('Create')); + } } }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 6799fb986aa..447cee42a75 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -60,6 +60,8 @@ "ignore_pricing_rule", "sec_warehouse", "set_warehouse", + "column_break_55", + "set_target_warehouse", "items_section", "update_stock", "scan_barcode", @@ -1969,13 +1971,24 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.is_internal_customer && doc.update_stock", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:48:31.769958", + "modified": "2020-12-25 22:57:32.555067", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bbc86c4e949..f45e076ce13 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -6,7 +6,7 @@ import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw -from erpnext.accounts.party import get_party_account, get_due_date +from erpnext.accounts.party import get_party_account, get_due_date, get_party_details from frappe.model.mapper import get_mapped_doc from erpnext.controllers.selling_controller import SellingController from erpnext.accounts.utils import get_account_currency @@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from frappe.model.utils import get_fetch_values +from frappe.contacts.doctype.address.address import get_address_display from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -1537,7 +1539,7 @@ def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) - if not valid_price_list: + if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) party = details.get("party") @@ -1560,6 +1562,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" source_document_warehouse_field = 'target_warehouse' target_document_warehouse_field = 'from_warehouse' else: @@ -1573,6 +1576,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name @@ -1580,19 +1584,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.supplier = details.get("party") + target_doc.is_internal_supplier = 1 + target_doc.ignore_pricing_rule = 1 target_doc.buying_price_list = source_doc.selling_price_list + # Invert Addresses + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + if currency: target_doc.currency = currency + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) + else: currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.selling_price_list = source_doc.buying_price_list + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + if currency: target_doc.currency = currency + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": [ @@ -1600,25 +1623,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "expense_account", "cost_center", "warehouse" - ] + ], + "field_map": { + 'rate': 'rate', + } } - if source_doc.get('update_stock'): - item_field_map.update({ - 'field_map': { - source_document_warehouse_field: target_document_warehouse_field, - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' - } + if doctype in ["Sales Invoice", "Sales Order"]: + item_field_map["field_map"].update({ + "name": target_detail_field, }) + if source_doc.get('update_stock'): + item_field_map["field_map"].update({ + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + }) doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, + "set_target_warehouse": "set_from_warehouse", "field_no_map": [ - "taxes_and_charges" + "taxes_and_charges", + "set_warehouse", + "shipping_address" ] }, doctype +" Item": item_field_map @@ -1627,6 +1658,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): return doclist +def set_purchase_references(doc): + # add internal PO or PR links if any + if doc.is_internal_transfer(): + if doc.doctype == 'Purchase Receipt': + so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) + + if so_item_map: + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map) + + elif doc.doctype == 'Purchase Invoice': + dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference) + # First check for Purchase receipt + if list(dn_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item') + + update_pi_items(doc, 'pr_detail', 'purchase_receipt', + dn_item_map, pd_item_map, parent_child_map, warehouse_map) + + if list(so_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pi_items(doc, 'po_detail', 'purchase_order', + so_item_map, pd_item_map, parent_child_map, warehouse_map) + +def update_pi_items(doc, detail_field, parent_field, sales_item_map, + purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item))) + item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) + if doc.update_stock: + item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + +def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) + item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) + item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + +def get_delivery_note_details(internal_reference): + so_item_map = {} + + si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'], + filters={'parent': internal_reference}) + + for d in si_item_details: + so_item_map.setdefault(d.name, d.so_detail) + + return so_item_map + +def get_sales_invoice_details(internal_reference): + dn_item_map = {} + so_item_map = {} + + si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail', + 'dn_detail'], filters={'parent': internal_reference}) + + for d in si_item_details: + if d.dn_detail: + dn_item_map.setdefault(d.name, d.dn_detail) + if d.so_detail: + so_item_map.setdefault(d.name, d.so_detail) + + return dn_item_map, so_item_map + +def get_pd_details(doctype, sd_detail_map, sd_detail_field): + pd_item_map = {} + accepted_warehouse_map = {} + parent_child_map = {} + + pd_item_details = frappe.get_all(doctype, + fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))}) + + for d in pd_item_details: + pd_item_map.setdefault(d.get(sd_detail_field), d.name) + parent_child_map.setdefault(d.get(sd_detail_field), d.parent) + accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse) + + return pd_item_map, parent_child_map, accepted_warehouse_map + +def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None, + company_address=None, shipping_address_name=None, master_doctype=None): + # Update Party Details + party_details = get_party_details(party=party, party_type=party_type, company=company, + doctype=doctype, party_address=party_address, company_address=company_address, + shipping_address=shipping_address_name) + + # Update taxes and charges if any + doc.taxes_and_charges = party_details.get('taxes_and_charges') + doc.set('taxes', party_details.get('taxes')) + +def update_address(doc, address_field, address_display_field, address_name): + doc.set(address_field, address_name) + fetch_values = get_fetch_values(doc.doctype, address_field, address_name) + + for key, value in fetch_values.items(): + doc.set(key, value) + + doc.set(address_display_field, get_address_display(doc.get(address_field))) + @frappe.whitelist() def get_loyalty_programs(customer): ''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ee532da4dc9..7cd1828343b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.utils import get_incoming_rate class TestSalesInvoice(unittest.TestCase): def make(self): @@ -1770,59 +1771,82 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") - # def test_internal_transfer_gl_entry(self): - # ## Create internal transfer account - # account = create_account(account_name="Unrealized Profit", - # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") - # frappe.db.set_value('Company', '_Test Company with perpetual inventory', - # 'unrealized_profit_loss_account', account) + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) - # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # si = create_sales_invoice( - # company = "_Test Company with perpetual inventory", - # customer = customer, - # debit_to = "Debtors - TCP1", - # warehouse = "Stores - TCP1", - # income_account = "Sales - TCP1", - # expense_account = "Cost of Goods Sold - TCP1", - # cost_center = "Main - TCP1", - # currency = "INR", - # do_not_save = 1 - # ) + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) - # si.selling_price_list = "_Test Price List Rest of the World" - # si.update_stock = 1 - # si.items[0].target_warehouse = 'Work In Progress - TCP1' - # add_taxes(si) - # si.save() - # si.submit() + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() - # target_doc = make_inter_company_transaction("Sales Invoice", si.name) - # target_doc.company = '_Test Company with perpetual inventory' - # target_doc.items[0].warehouse = 'Finished Goods - TCP1' - # add_taxes(target_doc) - # target_doc.save() - # target_doc.submit() + rate = 0.0 + for d in si.get('items'): + rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.serial_no, + "company": si.company, + "voucher_type": 'Sales Invoice', + "voucher_no": si.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) - # si_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], - # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] - # ] + rate = flt(rate, 2) - # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + si.submit() - # pi_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], - # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] - # ] + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() - # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + tax_amount = flt(rate * (12/100), 2) + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()], + ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()] + ] + + # Sale and Purchase both should be at valuation rate + self.assertEqual(si.items[0].rate, rate) + self.assertEqual(target_doc.items[0].rate, rate) + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 36950757989..7a98afff364 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -565,11 +565,12 @@ "print_hide": 1 }, { + "depends_on": "eval: parent.is_internal_customer && parent.update_stock", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -815,7 +816,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-23 19:59:04.879322", + "modified": "2020-12-26 17:25:04.090630", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b46de6c85bb..429a9f3591d 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc): validate_disabled(doc) + # Validate with existing taxes and charges template for unique tax category + validate_for_tax_category(doc) + for tax in doc.get("taxes"): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, doc) @@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc): def validate_disabled(doc): if doc.is_default and doc.disabled: frappe.throw(_("Disabled template must not be default template")) + +def validate_for_tax_category(doc): + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): + frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 9827e00b71b..8eef2adce3e 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -152,7 +152,7 @@ {{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }} {{ frappe.utils.fmt_money(0, None, "INR") }} {{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }} - {{ frappe.utils.fmt_money(0, None, "INR") }} + {{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }} {{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} {{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index eeb5140bbe2..cb4d9b43dbd 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -49,7 +49,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum elif d.po_detail: purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) - expense_account = d.expense_account or aii_account_map.get(d.company) + expense_account = d.unrealized_profit_loss_account or d.expense_account \ + or aii_account_map.get(d.company) row = { 'item_code': d.item_code, @@ -315,6 +316,7 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index f54ceb0d2f5..998003ac698 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum 'company': d.company, 'sales_order': d.sales_order, 'delivery_note': d.delivery_note, - 'income_account': d.income_account, + 'income_account': d.unrealized_profit_loss_account or d.income_account, 'cost_center': d.cost_center, 'stock_qty': d.stock_qty, 'stock_uom': d.stock_uom @@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns): select `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, + `tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 9399e707390..8ac749d6290 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if not filters: filters = {} invoice_list = get_invoices(filters, additional_query_columns) - columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \ + = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_expense_map = get_invoice_expense_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) @@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum # map expense values base_net_total = 0 for expense_acc in expense_accounts: - expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) + if inv.is_internal_supplier and inv.company == inv.represents_company: + expense_amount = 0 + else: + expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) base_net_total += expense_amount row.append(expense_amount) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.append(flt(internal_invoice_map.get((inv.name, account)))) + # net total row.append(base_net_total or inv.base_net_total) @@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns): "width": 80 } ] - expense_accounts = tax_accounts = expense_columns = tax_columns = [] + expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \ + unrealized_profit_loss_account_columns = [] if invoice_list: expense_accounts = frappe.db.sql_list("""select distinct expense_account @@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabPurchase Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] + unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] + for account in tax_accounts: if account not in expense_accounts: tax_columns.append(account + ":Currency/currency:120") - columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ + columns = columns + expense_columns + unrealized_profit_loss_account_columns + \ + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] - return columns, expense_accounts, tax_accounts + return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list): return invoice_expense_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabPurchase Invoice` where name in (%s) + and is_internal_supplier = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): tax_details = frappe.db.sql(""" select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index b6e61b13069..cb2c98b64ae 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No if not filters: filters = frappe._dict({}) invoice_list = get_invoices(filters, additional_query_columns) - columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_income_map = get_invoice_income_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts) #Cost Center & Warehouse Map @@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No # map income values base_net_total = 0 for income_acc in income_accounts: - income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + if inv.is_internal_customer and inv.company == inv.represents_company: + income_amount = 0 + else: + income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + base_net_total += income_amount row.update({ frappe.scrub(income_acc): income_amount }) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.update({ + frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account))) + }) + # net total row.update({'net_total': base_net_total or inv.base_net_total}) @@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns): tax_accounts = [] income_columns = [] tax_columns = [] + unrealized_profit_loss_accounts = [] + unrealized_profit_loss_account_columns = [] if invoice_list: income_accounts = frappe.db.sql_list("""select distinct income_account @@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabSales Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + for account in income_accounts: income_columns.append({ "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) @@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns): "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) + for account in unrealized_profit_loss_accounts: + unrealized_profit_loss_account_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }) + net_total_column = [{ "label": _("Net Total"), "fieldname": "net_total", "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }] @@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns): } ] - columns = columns + income_columns + net_total_column + tax_columns + total_columns + columns = columns + income_columns + unrealized_profit_loss_account_columns + \ + net_total_column + tax_columns + total_columns - return columns, income_accounts, tax_accounts + return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns): return frappe.db.sql(""" select name, posting_date, debit_to, project, customer, customer_name, owner, remarks, territory, tax_id, customer_group, - base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0} + base_net_total, base_grand_total, base_rounded_total, outstanding_amount, + is_internal_customer, represents_company, company {0} from `tabSales Invoice` where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % conditions, filters, as_dict=1) @@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list): return invoice_income_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabSales Invoice` where name in (%s) + and is_internal_customer = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): tax_details = frappe.db.sql("""select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 0b9827462c7..dd0f0658485 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -164,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => { - let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier); - let internal = supplier.is_internal_supplier; - let disabled = supplier.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(me.frm); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_supplier; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" : + "Inter Company Sales Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(me.frm); + }, __('Create')); + } + } } @@ -353,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( make_purchase_receipt: function() { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", - frm: cur_frm + frm: cur_frm, + freeze_message: __("Creating Purchase Receipt ...") }) }, @@ -380,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 75da71ceff8..ee2beea67f9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -134,6 +134,8 @@ "ref_sq", "column_break_74", "party_account_currency", + "is_internal_supplier", + "represents_company", "inter_company_order_reference" ], "fields": [ @@ -1101,13 +1103,28 @@ { "fieldname": "items_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "supplier.is_internal_supplier", + "fieldname": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-12-03 16:46:44.229351", + "modified": "2021-01-20 22:07:23.487138", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a17..d32e98e8d94 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ - .format(item.item_code, item.idx))) + frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") + .format(item.item_code, item.idx)) def get_schedule_dates(self): for d in self.get('items'): diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index e537771eaf2..b76c3784a47 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) @@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99] + per_ordered: ["<", 100] } }); dialog.hide(); diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 0ee9d180d99..edeb135d951 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -52,7 +52,10 @@ class Supplier(TransactionBase): self.validate_internal_supplier() def validate_internal_supplier(self): - if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + internal_supplier = frappe.db.get_value("Supplier", + {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_supplier: frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index a3b2085400e..a0187b0a824 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0f1aa23064c..9d9d1b363a9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -75,6 +75,9 @@ class AccountsController(TransactionBase): self.ensure_supplier_is_not_blocked() self.validate_date_with_fiscal_year() + self.validate_inter_company_reference() + + self.set_incoming_rate() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -110,14 +113,20 @@ class AccountsController(TransactionBase): self.set_inter_company_account() validate_regional(self) - + validate_einvoice_fields(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) - + def before_cancel(self): validate_einvoice_fields(self) + + def on_trash(self): + # delete sl and gl entries on deletion of transaction + if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): + frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) + frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) def validate_deferred_start_and_end_date(self): for d in self.items: @@ -206,6 +215,17 @@ class AccountsController(TransactionBase): validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, self.meta.get_label(date_field), self) + def validate_inter_company_reference(self): + if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): + return + + if self.is_internal_transfer(): + if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference') + or self.get('inter_company_order_reference')): + msg = _("Internal Sale or Delivery Reference missing. ") + msg += _("Please create purchase from internal sale or delivery document itself") + frappe.throw(msg, title=_("Internal Sales Reference Missing")) + def validate_due_date(self): if self.get('is_pos'): return @@ -448,8 +468,10 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(gl_dict.account) if gl_dict.account and self.doctype not in ["Journal Entry", - "Period Closing Voucher", "Payment Entry"]: + "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]: self.validate_account_currency(gl_dict.account, account_currency) + + if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]: set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), self.company_currency) @@ -962,9 +984,9 @@ class AccountsController(TransactionBase): It will an internal transfer if its an internal customer and representation company is same as billing company """ - if self.doctype == 'Sales Invoice': + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'): internal_party_field = 'is_internal_customer' - else: + elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): internal_party_field = 'is_internal_supplier' if self.get(internal_party_field) and (self.represents_company == self.company): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4dee375e5a2..ab1f02779be 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -44,7 +44,6 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() - self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -100,11 +99,6 @@ class BuyingController(StockController): msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') self.update_tax_category(msg) - def update_tax_category_for_internal_transfer(self): - if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): - msg = _('Tax Category has been changed to "Total" as its an internal purchase.') - self.update_tax_category(msg) - def update_tax_category(self, msg): tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] @@ -224,6 +218,48 @@ class BuyingController(StockController): else: item.valuation_rate = 0.0 + def set_incoming_rate(self): + if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): + return + + ref_doctype_map = { + "Purchase Order": "Sales Order Item", + "Purchase Receipt": "Delivery Note Item", + "Purchase Invoice": "Sales Invoice Item", + } + + ref_doctype = ref_doctype_map.get(self.doctype) + items = self.get("items") + for d in items: + if not cint(self.get("is_return")): + # Get outgoing rate based on original item cost based on valuation method + + if not d.get(frappe.scrub(ref_doctype)): + outgoing_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.get('from_warehouse'), + "posting_date": self.get('posting_date') or self.get('transation_date'), + "posting_time": self.get('posting_time'), + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) + + rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate')) + else: + rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate') + + if self.is_internal_transfer(): + if rate != d.rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for d in self.get("supplied_items"): @@ -243,7 +279,7 @@ class BuyingController(StockController): d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) - + return supplied_items_cost def validate_for_subcontracting(self): @@ -559,6 +595,8 @@ class BuyingController(StockController): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, + "outgoing_rate": d.rate, + "recalculate_rate": 1, "dependant_sle_voucher_detail_no": d.name }) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6f9e1ef3d3a..e085048f99b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form +from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime from frappe import _, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate @@ -49,7 +49,6 @@ class SellingController(StockController): self.set_customer_address() self.validate_for_duplicate_items() self.validate_target_warehouse() - self.set_incoming_rate() def set_missing_values(self, for_validate=False): @@ -312,7 +311,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice"): + if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): return items = self.get("items") + (self.get("packed_items") or []) @@ -322,15 +321,26 @@ class SellingController(StockController): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1*flt(d.qty), - "serial_no": d.serial_no, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')), + "serial_no": d.get('serial_no'), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, "allow_zero_valuation": d.get("allow_zero_valuation") }, raise_error_if_no_rate=False) + + # For internal transfers use incoming rate as the valuation rate + if self.is_internal_transfer(): + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + elif self.get("return_against"): # Get incoming rate of return entry from reference document # based on original item cost as per valuation method diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 439997616c7..4b5e3479706 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,6 +6,7 @@ import frappe, erpnext from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults +from collections import defaultdict from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController @@ -23,6 +24,8 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.validate_internal_transfer() + self.validate_putaway_capacity() def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: @@ -72,6 +75,7 @@ class StockController(AccountsController): warehouse_with_no_account = [] precision = frappe.get_precision("GL Entry", "debit_in_account_currency") for item_row in voucher_details: + sle_list = sle_map.get(item_row.name) if sle_list: for sle in sle_list: @@ -216,7 +220,7 @@ class StockController(AccountsController): """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: - stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) + stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger def make_batches(self, warehouse_field): @@ -391,6 +395,84 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def validate_internal_transfer(self): + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ + and self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + + def validate_in_transit_warehouses(self): + if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note': + for item in self.get('items'): + if not item.target_warehouse: + frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx)) + + if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt': + for item in self.get('items'): + if not item.from_warehouse: + frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx)) + + def validate_multi_currency(self): + if self.currency != self.company_currency: + frappe.throw(_("Internal transfers can only be done in company's default currency")) + + def validate_packed_items(self): + if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'): + frappe.throw(_("Packed Items cannot be transferred internally")) + + def validate_putaway_capacity(self): + # if over receipt is attempted while 'apply putaway rule' is disabled + # and if rule was applied on the transaction, validate it. + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice", + "Stock Reconciliation") + + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: + valid_doctype = False + + if valid_doctype: + rule_map = defaultdict(dict) + for item in self.get("items"): + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + rule = frappe.db.get_value("Putaway Rule", + { + "item_code": item.get("item_code"), + "warehouse": item.get(warehouse_field) + }, + ["name", "disable"], as_dict=True) + if rule: + if rule.get("disabled"): continue # dont validate for disabled rule + + if self.doctype == "Stock Reconciliation": + stock_qty = flt(item.qty) + else: + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + + rule_name = rule.get("name") + if not rule_map[rule_name]: + rule_map[rule_name]["warehouse"] = item.get(warehouse_field) + rule_map[rule_name]["item"] = item.get("item_code") + rule_map[rule_name]["qty_put"] = 0 + rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name) + rule_map[rule_name]["qty_put"] += flt(stock_qty) + + for rule, values in rule_map.items(): + if flt(values["qty_put"]) > flt(values["capacity"]): + message = self.prepare_over_receipt_message(rule, values) + frappe.throw(msg=message, title=_("Over Receipt")) + + def prepare_over_receipt_message(self, rule, values): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

" + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + return message + def repost_future_sle_and_gle(self): args = frappe._dict({ "posting_date": self.posting_date, diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8dd2e5bacbd..76309f8799a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules +from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -758,3 +759,35 @@ def get_rounded_tax_amount(itemised_tax, precision): for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + +class init_landed_taxes_and_totals(object): + def __init__(self, doc): + self.doc = doc + self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs' + self.set_account_currency() + self.set_exchange_rate() + self.set_amounts_in_company_currency() + + def set_account_currency(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if not d.account_currency: + account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') + d.account_currency = account_currency or company_currency + + def set_exchange_rate(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if d.account_currency == company_currency: + d.exchange_rate = 1 + elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: + d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, + account_currency=d.account_currency, company=self.doc.company) + + if not d.exchange_rate: + frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) + + def set_amounts_in_company_currency(self): + for d in self.doc.get(self.tax_field): + d.amount = flt(d.amount, d.precision("amount")) + d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 14377e1b994..1c20555b827 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -416,9 +416,6 @@ regional_overrides = { 'Italy': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', - }, - 'Germany': { - 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional', } } user_privacy_documents = [ diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 2c385e80f46..ab65260c091 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -88,7 +88,7 @@ def get_events(start, end, filters=None): def add_assignments(events, start, end, conditions=None): query = """select name, start_date, end_date, employee_name, - employee, docstatus + employee, docstatus, shift_type from `tabShift Assignment` where start_date >= %(start_date)s or end_date <= %(end_date)s @@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None): if conditions: query += conditions - for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): - e = { - "name": d.name, - "doctype": "Shift Assignment", - "start_date": d.start_date, - "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + ": "+ \ - cstr(d.shift_type), - "docstatus": d.docstatus - } - if e not in events: - events.append(e) + records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True) + shift_timing_map = get_shift_type_timing([d.shift_type for d in records]) + + for d in records: + daily_event_start = d.start_date + daily_event_end = d.end_date if d.end_date else getdate() + delta = timedelta(days=1) + while daily_event_start <= daily_event_end: + start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time'] + end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time'] + daily_event_start += delta + e = { + "name": d.name, + "doctype": "Shift Assignment", + "start_date": start_timing, + "end_date": end_timing, + "title": cstr(d.employee_name) + ": "+ \ + cstr(d.shift_type), + "docstatus": d.docstatus, + "allDay": 0 + } + if e not in events: + events.append(e) + + return events + +def get_shift_type_timing(shift_types): + shift_timing_map = {} + data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time']) + + for d in data: + shift_timing_map[d.name] = d + + return shift_timing_map def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None): diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index 17a986deb21..bb692e1402e 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = { "start": "start_date", "end": "end_date", "id": "name", - "docstatus": 1 - }, - options: { - header: { - left: 'prev,next today', - center: 'title', - right: 'month' - } + "docstatus": 1, + "allDay": "allDay", }, get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events" } \ No newline at end of file diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index f280402ce9b..ab586bc09c4 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -43,22 +43,24 @@ def get_data(filters): currency = erpnext.get_company_currency(filters.get('company')) for key, qty in iteritems(pledge_values): - row = {} - current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') + if qty: + row = {} + current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') - row.update(loan_security_details.get(key[1])) - row.update({ - 'applicant_type': applicant_type_map.get(key[0]), - 'applicant_name': key[0], - 'total_qty': qty, - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), - 'currency': currency - }) + row.update(loan_security_details.get(key[1])) + row.update({ + 'applicant_type': applicant_type_map.get(key[0]), + 'applicant_name': key[0], + 'total_qty': qty, + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \ + else 0.0, + 'currency': currency + }) - data.append(row) + data.append(row) return data diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index ff88052df5b..adc8013c686 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -40,21 +40,22 @@ def get_data(filters): currency = erpnext.get_company_currency(filters.get('company')) for security, value in iteritems(current_pledges): - row = {} - current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(security, {}).get('valid_upto') + if value.get('qty'): + row = {} + current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(security, {}).get('valid_upto') - row.update(loan_security_details.get(security)) - row.update({ - 'total_qty': value.get('qty'), - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), - 'pledged_applicant_count': value.get('applicant_count'), - 'currency': currency - }) + row.update(loan_security_details.get(security)) + row.update({ + 'total_qty': value.get('qty'), + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), + 'pledged_applicant_count': value.get('applicant_count'), + 'currency': currency + }) - data.append(row) + data.append(row) return data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index da52ae9aff8..f3660b3c51c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -747,3 +747,4 @@ erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes +erpnext.patches.v13_0.add_naming_series_to_old_projects diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py new file mode 100644 index 00000000000..79b67533ed6 --- /dev/null +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter + +def execute(): + frappe.reload_doc("projects", "doctype", "project") + projects = frappe.db.get_all("Project", + fields=["name", "naming_series", "modified"], + filters={ + "naming_series": ["is", "not set"] + }, + order_by="timestamp(modified) asc") + + # disable set only once as the old docs must be saved + # (to bypass 'Cant change naming series' validation on save) + make_property_setter("Project", "naming_series", "set_only_once", 0, "Check") + + for entry in projects: + # need to save the doc so that users can edit old projects + doc = frappe.get_doc("Project", entry.name) + if not doc.naming_series: + doc.naming_series = "PROJ-.####" + doc.save() + + delete_property_setter("Project", "set_only_once", "naming_series") + frappe.db.commit() diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d20c98c0984..d1ed91fac70 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -53,8 +53,7 @@ frappe.ui.form.on('Additional Salary', { if (!frm.doc.company) return; frm.set_query("salary_component", function() { return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", company: frm.doc.company} + filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} }; }); }, diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index 182ce0f83a6..b2809b164a0 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -10,15 +10,7 @@ frappe.ui.form.on('Employee Incentive', { } }; }); - - if (!frm.doc.company) return; - frm.set_query("salary_component", function() { - return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", company: frm.doc.company} - }; - }); - + frm.trigger('set_earning_component'); }, employee: function(frm) { @@ -45,11 +37,21 @@ frappe.ui.form.on('Employee Incentive', { callback: function(data) { if (data.message) { frm.set_value("company", data.message.company); + frm.trigger('set_earning_component'); } } }); }, + set_earning_component: function(frm) { + if (!frm.doc.company) return; + frm.set_query("salary_component", function() { + return { + filters: {type: "earning", company: frm.doc.company} + }; + }); + }, + get_employee_currency: function(frm) { frappe.call({ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index 6c7b382264f..1378bf0b913 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -58,13 +58,11 @@ frappe.ui.form.on('Salary Structure', { if(!frm.doc.company) return; frm.set_query("salary_component", "earnings", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "earning", company: frm.doc.company} }; }); frm.set_query("salary_component", "deductions", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "deduction", company: frm.doc.company} }; }); diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index e71803172c5..17120815504 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -207,22 +207,3 @@ def get_employees(salary_structure): return list(set([d.employee for d in employees])) -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): - if len(filters) < 2: - return {} - - return frappe.db.sql(""" - select t1.salary_component - from `tabSalary Component` t1, `tabSalary Component Account` t2 - where (t1.name = t2.parent - and t1.type = %(type)s - and t2.company = %(company)s) - or (t1.type = %(type)s - and t1.statistical_component = 1) - order by salary_component - """,{ - "type": filters['type'], - "company": filters['company'] - }) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2f15cbcef1a..f0212db0b21 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -55,6 +55,8 @@ "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" + "stock/dashboard/item_dashboard.js", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html" ] } diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index db85a3ec99e..a2a723dd774 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -195,6 +195,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this._super(doc, cdt, cdn); }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + received_qty: function(doc, cdt, cdn) { this.calculate_accepted_qty(doc, cdt, cdn) }, @@ -516,4 +520,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); -} +} \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f144c290f5e..9627600a17a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -105,10 +105,18 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); - if(!item.warehouse && frm.doc.set_warehouse) { + if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; } + if (!item.target_warehouse && frm.doc.set_target_warehouse) { + item.target_warehouse = frm.doc.set_target_warehouse; + } + + if (!item.from_warehouse && frm.doc.set_from_warehouse) { + item.from_warehouse = frm.doc.set_from_warehouse; + } + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); @@ -227,6 +235,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }; + this.frm.trigger('set_default_internal_warehouse'); + return frappe.run_serially([ () => set_value('currency', currency), () => set_value('price_list_currency', currency), @@ -589,11 +599,21 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { if (r.message && - (r.message.has_batch_no || r.message.has_serial_no)) { + (r.message.has_batch_no || r.message.has_serial_no)) { frappe.flags.hide_serial_batch_dialog = false; } }); }, + () => { + // check if batch serial selector is disabled or not + if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) + return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector') + .then((value) => { + if (value) { + frappe.flags.hide_serial_batch_dialog = true; + } + }); + }, () => { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { var d = locals[cdt][cdn]; @@ -648,7 +668,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ args: item_args }, callback: function(r) { - frappe.model.set_value(item.doctype, item.name, 'rate', r.message); + frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor); } }); }, @@ -714,6 +734,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.calculate_taxes_and_totals(false); }, + update_stock: function() { + this.frm.trigger('set_default_internal_warehouse'); + }, + + set_default_internal_warehouse: function() { + let me = this; + if ((this.frm.doc.doctype === 'Sales Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Delivery Note') { + if (this.frm.doc.is_internal_customer && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_target_warehouse', value.default_in_transit_warehouse); + }); + } + } + + if ((this.frm.doc.doctype === 'Purchase Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Purchase Receipt') { + if (this.frm.doc.is_internal_supplier && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_from_warehouse', value.default_in_transit_warehouse); + }); + } + } + }, + company: function() { var me = this; var set_pricing = function() { @@ -800,7 +845,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); - }) + }); // Get default company billing address in Purchase Invoice, Order and Receipt frappe.call({ @@ -1099,6 +1144,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + batch_no: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + this.apply_price_list(item, true); + }, + toggle_conversion_factor: function(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1403,6 +1453,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "pricing_rules": d.pricing_rules, "warehouse": d.warehouse, "serial_no": d.serial_no, + "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0 }); @@ -1961,6 +2012,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); }, + set_target_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); + }, + + set_from_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "from_warehouse", this.frm.doc.set_from_warehouse); + }, + autofill_warehouse : function (child_table, warehouse_field, warehouse) { if (warehouse && child_table && child_table.length) { let doctype = child_table[0].doctype; @@ -2025,3 +2084,35 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }, show_dialog); }); } + +erpnext.apply_putaway_rule = (frm, purpose=null) => { + if (!frm.doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + doctype: frm.doctype, + items: frm.doc.items, + company: frm.doc.company, + sync: true, + purpose: purpose + }, + callback: (result) => { + if (!result.exc && result.message) { + frm.clear_table("items"); + + let items = result.message; + items.forEach((row) => { + delete row["name"]; // dont overwrite name from server side + let child = frm.add_child("items"); + Object.assign(child, row); + frm.script_manager.trigger("qty", child.doctype, child.name); + }); + frm.get_field("items").grid.refresh(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 770704e595b..808dd5add05 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,6 +276,12 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback){ if (frm.doc.company) { + if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + frm.doc.internal_order_reference)) { + if (callback) { + return callback(); + } + } frappe.call({ method: "erpnext.accounts.custom.address.get_shipping_address", args: { diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 713e8e34ef6..f60de4c8af4 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -7,13 +7,14 @@ "engine": "InnoDB", "field_order": [ "client", - "account_number_length", - "column_break_2", "client_number", - "section_break_4", + "column_break_2", + "consultant_number", "consultant", + "section_break_4", + "account_number_length", "column_break_6", - "consultant_number" + "temporary_against_account_number" ], "fields": [ { @@ -66,10 +67,17 @@ "fieldtype": "Int", "label": "Account Number Length", "reqd": 1 + }, + { + "allow_in_quick_entry": 1, + "fieldname": "temporary_against_account_number", + "fieldtype": "Data", + "label": "Temporary Against Account Number", + "reqd": 1 } ], "links": [], - "modified": "2020-11-05 17:52:11.674329", + "modified": "2020-11-19 19:00:09.088816", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py deleted file mode 100644 index 7f76493608e..00000000000 --- a/erpnext/regional/germany/accounts_controller.py +++ /dev/null @@ -1,53 +0,0 @@ -import frappe -from frappe import _ -from frappe import msgprint - - -REQUIRED_FIELDS = { - "Sales Invoice": [ - { - "field_name": "company_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG" - }, - { - "field_name": "company_tax_id", - "regulation": "§ 14 Abs. 4 Nr. 2 UStG" - }, - { - "field_name": "taxes", - "regulation": "§ 14 Abs. 4 Nr. 8 UStG" - }, - { - "field_name": "customer_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG", - "condition": "base_grand_total > 250" - } - ] -} - - -def validate_regional(doc): - """Check if required fields for this document are present.""" - required_fields = REQUIRED_FIELDS.get(doc.doctype) - if not required_fields: - return - - meta = frappe.get_meta(doc.doctype) - field_map = {field.fieldname: field.label for field in meta.fields} - - for field in required_fields: - condition = field.get("condition") - if condition and not frappe.safe_eval(condition, doc.as_dict()): - continue - - field_name = field.get("field_name") - regulation = field.get("regulation") - if field_name and not doc.get(field_name): - missing(field_map.get(field_name), regulation) - - -def missing(field_label, regulation): - """Notify the user that a required field is missing.""" - translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501 - formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) - msgprint(formatted_msg) diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py deleted file mode 100644 index 8bd378c971f..00000000000 --- a/erpnext/regional/germany/test_accounts_controller.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe -import unittest -from erpnext.regional.germany.accounts_controller import validate_regional - - -class TestAccountsController(unittest.TestCase): - - def setUp(self): - self.sales_invoice = frappe.get_last_doc('Sales Invoice') - - def test_validate_regional(self): - validate_regional(self.sales_invoice) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 87baece65d3..f09d3d08ad2 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -40,14 +40,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => { callback: function(r) { if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + frm.set_value('taxes', r.message.taxes); frm.set_value('place_of_supply', r.message.place_of_supply); - } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) { - frm.set_value('taxes_and_charges', ''); - frm.set_value('taxes', []); } } }); } }); -}; +} diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 46693acd08a..d6200c9fd63 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -171,7 +171,7 @@ def get_regional_address_details(party_details, doctype, company): if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' - party_details.taxes = '' + party_details.taxes = [] return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 1e39c57786c..cbc94789874 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -96,6 +96,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): + fn = 'temporary_against_account_number' + filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -156,11 +158,11 @@ def get_transactions(filters, as_dict=1): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', /* account number or, if empty, party account number */ - coalesce(acc.account_number, acc_pa.account_number) as 'Konto', + acc.account_number as 'Konto', /* against number or, if empty, party against number */ - coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', - + %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', LEFT(gl.remarks, 60) as 'Buchungstext', @@ -171,27 +173,10 @@ def get_transactions(filters, as_dict=1): FROM `tabGL Entry` gl - /* Statistisches Konto (Debitoren/Kreditoren) */ - left join `tabParty Account` pa - on gl.against = pa.parent - and gl.company = pa.company - /* Kontonummer */ left join `tabAccount` acc on gl.account = acc.name - /* Gegenkonto-Nummer */ - left join `tabAccount` acc_against - on gl.against = acc_against.name - - /* Statistische Kontonummer */ - left join `tabAccount` acc_pa - on pa.account = acc_pa.name - - /* Statistische Gegenkonto-Nummer */ - left join `tabAccount` acc_against_pa - on pa.account = acc_against_pa.name - WHERE gl.company = %(company)s AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) <= %(to_date)s @@ -347,7 +332,9 @@ def download_datev_csv(filters): coa = frappe.get_value('Company', company, 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') - filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length') + datev_settings = frappe.get_doc('DATEV Settings', company) + filters['account_number_length'] = datev_settings.account_number_length + filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 9529923a73b..59b878e94a5 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -126,7 +126,8 @@ def make_datev_settings(company): "doctype": "DATEV Settings", "client": company.name, "client_number": "12345", - "consultant_number": "67890" + "consultant_number": "67890", + "temporary_against_account_number": "9999" }).insert() @@ -137,7 +138,8 @@ class TestDatev(TestCase): self.filters = { "company": self.company.name, "from_date": today(), - "to_date": today() + "to_date": today(), + "temporary_against_account_number": "9999" } make_datev_settings(self.company) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index ad3de5f398d..96dc3f728d9 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -255,15 +255,16 @@ class Gstr1Report(object): for item_code, tax_amounts in item_wise_tax_detail.items(): tax_rate = tax_amounts[0] - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) + if tax_rate: + if cgst_or_sgst: + tax_rate *= 2 + if parent not in self.cgst_sgst_invoices: + self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) + rate_based_dict = self.items_based_on_tax_rate\ + .setdefault(parent, {}).setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 29214ee06d9..bf8b7fc128a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,7 +84,10 @@ class Customer(TransactionBase): frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) def validate_internal_customer(self): - if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + internal_customer = frappe.db.get_value("Customer", + {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_customer: frappe.throw(_("Internal Customer for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d4fb07cc275..78f9df9588a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -171,8 +171,10 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); } - // make purchase order + // Make Purchase Order + if (!this.frm.doc.is_internal_customer) { this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create')); + } // maintenance if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { @@ -193,16 +195,15 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Customer", me.frm.doc.customer, () => { - let customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - let internal = customer.is_internal_customer; - let disabled = customer.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Order" : + "Inter Company Purchase Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(); + }, __('Create')); + } } } // payment request diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 3d64ac3780e..0a5c6651ba3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -107,6 +107,8 @@ "tc_name", "terms", "more_info", + "is_internal_customer", + "represents_company", "inter_company_order_reference", "project", "party_account_currency", @@ -1103,7 +1105,8 @@ "hide_days": 1, "hide_seconds": 1, "label": "Inter Company Order Reference", - "options": "Purchase Order" + "options": "Purchase Order", + "read_only": 1 }, { "description": "Track this Sales Order against any Project", @@ -1455,13 +1458,29 @@ "hide_seconds": 1, "label": "Skip Delivery Note", "print_hide": 1 + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "is_internal_customer", + "fieldtype": "Check", + "label": "Is Internal Customer", + "read_only": 1 + }, + { + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:59:18.628077", + "modified": "2021-01-20 23:40:39.929296", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 7f00fca8f05..ce084646e15 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,6 +399,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ } }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + qty: function(doc, cdt, cdn) { this._super(doc, cdt, cdn); diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 21ecaa14d23..9f50aba61cc 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -178,6 +178,13 @@ def create_lead_for_item_inquiry(lead, subject, message): lead_doc.update(lead) lead_doc.set('lead_owner', '') + if not frappe.db.exists('Lead Source', 'Product Inquiry'): + frappe.get_doc({ + 'doctype': 'Lead Source', + 'source_name' : 'Product Inquiry' + }).insert(ignore_permissions=True) + lead_doc.set('source', 'Product Inquiry') + try: lead_doc.save(ignore_permissions=True) except frappe.exceptions.DuplicateEntryError: diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 9bd03d45cbb..d3c442d3039 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({ handle_move_add($(this), "Add") }); + this.content.on('click', '.btn-edit', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + let company = unescape($(this).attr('data-company')); + frappe.db.get_value('Putaway Rule', + {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + }); + }); + function handle_move_add(element, action) { let item = unescape(element.attr('data-item')); let warehouse = unescape(element.attr('data-warehouse')); @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += 20; + me.start += me.page_length; me.refresh(); }); @@ -69,33 +79,43 @@ erpnext.stock.ItemDashboard = Class.extend({ this.before_refresh(); } + let args = { + item_code: this.item_code, + warehouse: this.warehouse, + parent_warehouse: this.parent_warehouse, + item_group: this.item_group, + company: this.company, + start: this.start, + sort_by: this.sort_by, + sort_order: this.sort_order + }; + var me = this; frappe.call({ - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - args: { - item_code: this.item_code, - warehouse: this.warehouse, - item_group: this.item_group, - start: this.start, - sort_by: this.sort_by, - sort_order: this.sort_order, - }, + method: this.method, + args: args, callback: function(r) { me.render(r.message); } }); }, render: function(data) { - if(this.start===0) { + if (this.start===0) { this.max_count = 0; this.result.empty(); } - var context = this.get_item_dashboard_data(data, this.max_count, true); + let context = ""; + if (this.page_name === "warehouse-capacity-summary") { + context = this.get_capacity_dashboard_data(data); + } else { + context = this.get_item_dashboard_data(data, this.max_count, true); + } + this.max_count = this.max_count; // show more button - if(data && data.length===21) { + if (data && data.length===(this.page_length + 1)) { this.content.find('.more').removeClass('hidden'); // remove the last element @@ -106,12 +126,17 @@ erpnext.stock.ItemDashboard = Class.extend({ // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); + this.content.find('.result').css('text-align', 'unset'); + $(frappe.render_template(this.template, context)).appendTo(this.result); } else { - var message = __("Currently no stock available in any warehouse"); - $(` ${message} `).appendTo(this.result); + var message = __("No Stock Available Currently"); + this.content.find('.result').css('text-align', 'center'); + + $(`
+ ${message}
`).appendTo(this.result); } }, + get_item_dashboard_data: function(data, max_count, show_item) { if(!max_count) max_count = 0; if(!data) data = []; @@ -128,8 +153,8 @@ erpnext.stock.ItemDashboard = Class.extend({ d.total_reserved, max_count); }); - var can_write = 0; - if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ + let can_write = 0; + if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) { can_write = 1; } @@ -138,9 +163,27 @@ erpnext.stock.ItemDashboard = Class.extend({ max_count: max_count, can_write:can_write, show_item: show_item || false + }; + }, + + get_capacity_dashboard_data: function(data) { + if (!data) data = []; + + data.forEach(function(d) { + d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; + }); + + let can_write = 0; + if (frappe.boot.user.can_write.indexOf("Putaway Rule") >= 0) { + can_write = 1; } + + return { + data: data, + can_write: can_write, + }; } -}) +}); erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py new file mode 100644 index 00000000000..ab573e566ac --- /dev/null +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import frappe +from frappe.model.db_query import DatabaseQuery +from frappe.utils import nowdate +from frappe.utils import flt +from erpnext.stock.utils import get_stock_balance + +@frappe.whitelist() +def get_data(item_code=None, warehouse=None, parent_warehouse=None, + company=None, start=0, sort_by="stock_capacity", sort_order="desc"): + """Return data to render the warehouse capacity dashboard.""" + filters = get_filters(item_code, warehouse, parent_warehouse, company) + + no_permission, filters = get_warehouse_filter_based_on_permissions(filters) + if no_permission: + return [] + + capacity_data = get_warehouse_capacity_data(filters, start) + + asc_desc = -1 if sort_order == "desc" else 1 + capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + + return capacity_data + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, + company=None): + filters = [['disable', '=', 0]] + if item_code: + filters.append(['item_code', '=', item_code]) + if warehouse: + filters.append(['warehouse', '=', warehouse]) + if company: + filters.append(['company', '=', company]) + if parent_warehouse: + lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) + warehouses = frappe.db.sql_list(""" + select name from `tabWarehouse` + where lft >=%s and rgt<=%s + """, (lft, rgt)) + filters.append(['warehouse', 'in', warehouses]) + return filters + +def get_warehouse_filter_based_on_permissions(filters): + try: + # check if user has any restrictions based on user permissions on warehouse + if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): + filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + return False, filters + except frappe.PermissionError: + # user does not have access on warehouse + return True, [] + +def get_warehouse_capacity_data(filters, start): + capacity_data = frappe.db.get_all('Putaway Rule', + fields=['item_code', 'warehouse','stock_capacity', 'company'], + filters=filters, + limit_start=start, + limit_page_length='11' + ) + + for entry in capacity_data: + balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 + entry.update({ + 'actual_qty': balance_qty, + 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) + }) + + return capacity_data \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 9068e338c30..74cc42d1fba 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,12 +8,12 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Putaway Rule\",\n \"name\": \"Putaway Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, "label": "Stock Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Putaway Rule\"\n ],\n \"label\": \"Warehouse Capacity Summary\",\n \"name\": \"warehouse-capacity-summary\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-12-02 15:47:41.532942", + "modified": "2020-12-08 15:47:41.532942", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e41f1a8aaaf..97f85bafd95 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,6 +8,8 @@ import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from frappe.utils import cint, flt +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.stock.get_item_details import get_item_details class TestBatch(unittest.TestCase): def test_item_has_batch_enabled(self): @@ -182,7 +184,7 @@ class TestBatch(unittest.TestCase): stock_entry.cancel() current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty) - + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -252,6 +254,72 @@ class TestBatch(unittest.TestCase): return batch + def test_batch_wise_item_price(self): + if not frappe.db.get_value('Item', '_Test Batch Price Item'): + frappe.get_doc({ + 'doctype': 'Item', + 'is_stock_item': 1, + 'item_code': '_Test Batch Price Item', + 'item_group': 'Products', + 'has_batch_no': 1, + 'create_new_batch': 1 + }).insert(ignore_permissions=True) + + batch1 = create_batch('_Test Batch Price Item', 200, 1) + batch2 = create_batch('_Test Batch Price Item', 300, 1) + batch3 = create_batch('_Test Batch Price Item', 400, 0) + + args = frappe._dict({ + "item_code": "_Test Batch Price Item", + "company": "_Test Company with perpetual inventory", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None + }) + + #test price for batch1 + args.update({'batch_no': batch1}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 200) + + #test price for batch2 + args.update({'batch_no': batch2}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 300) + + #test price for batch3 + args.update({'batch_no': batch3}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 400) + +def create_batch(item_code, rate, create_item_price_for_batch): + pi = make_purchase_invoice(company="_Test Company with perpetual inventory", + warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + + batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + + if not create_item_price_for_batch: + create_price_list_for_batch(item_code, None, rate) + else: + create_price_list_for_batch(item_code, batch, rate) + + return batch + +def create_price_list_for_batch(item_code, batch, rate): + frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Batch Price Item', + 'price_list': '_Test Price List', + 'batch_no': batch, + 'price_list_rate': rate + }).insert() + def make_new_batch(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ee18042fdd5..334bdeac9d3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -95,13 +95,19 @@ frappe.ui.form.on("Delivery Note", { frm.page.set_inner_btn_group_as_primary(__('Create')); } - if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) { - frm.add_custom_button(__('Purchase Receipt'), function() { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', - frm: frm, - }) - }, __('Create')); + if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : + "Inter Company Purchase Receipt"; + + me.frm.add_custom_button(button_label, function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', + frm: frm, + }); + }, __('Create')); + } } } }); @@ -297,15 +303,6 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }) }, - - to_warehouse: function() { - let packed_items_table = this.frm.doc["packed_items"]; - this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse); - if (packed_items_table && packed_items_table.length) { - this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse); - } - } - }); $.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm})); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index c9f8d0810e3..f595aade917 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -53,7 +53,7 @@ "sec_warehouse", "set_warehouse", "col_break_warehouse", - "to_warehouse", + "set_target_warehouse", "items_section", "scan_barcode", "items", @@ -117,6 +117,7 @@ "source", "column_break5", "is_internal_customer", + "represents_company", "inter_company_reference", "per_billed", "customer_group", @@ -502,18 +503,6 @@ "fieldname": "col_break_warehouse", "fieldtype": "Column Break" }, - { - "description": "Required only for sample item.", - "fieldname": "to_warehouse", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "To Warehouse", - "no_copy": 1, - "oldfieldname": "to_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "print_hide": 1 - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1261,13 +1250,34 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_customer", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Set Target Warehouse", + "no_copy": 1, + "oldfieldname": "to_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "print_hide": 1 + }, + { + "description": "Company which internal customer represents.", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-11-30 12:54:45.407289", + "modified": "2020-12-26 17:07:59.194403", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a30cadf0a04..fa5a7fbe719 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -664,7 +664,8 @@ def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) def make_inter_company_transaction(doctype, source_name, target_doc=None): - from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_transaction, get_inter_company_details + from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_transaction, + get_inter_company_details, update_address, update_taxes, set_purchase_references) if doctype == 'Delivery Note': source_doc = frappe.get_doc(doctype, source_name) @@ -682,6 +683,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) if target.doctype == 'Purchase Receipt': master_doctype = 'Purchase Taxes and Charges Template' @@ -697,21 +699,35 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if target_doc.doctype == 'Purchase Receipt': target_doc.company = details.get("company") target_doc.supplier = details.get("party") - target_doc.supplier_address = source_doc.company_address - target_doc.shipping_address = source_doc.shipping_address_name or source_doc.customer_address target_doc.buying_price_list = source_doc.selling_price_list target_doc.is_internal_supplier = 1 target_doc.inter_company_reference = source_doc.name + + # Invert the address on target doc creation + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.company_address = source_doc.supplier_address - target_doc.shipping_address_name = source_doc.shipping_address target_doc.selling_price_list = source_doc.buying_price_list target_doc.is_internal_customer = 1 target_doc.inter_company_reference = source_doc.name - doclist = get_mapped_doc(doctype, source_name, { + # Invert the address on target doc creation + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, @@ -722,7 +738,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): doctype +" Item": { "doctype": target_doctype + " Item", "field_map": { - source_document_warehouse_field: target_document_warehouse_field + source_document_warehouse_field: target_document_warehouse_field, + 'name': 'delivery_note_item', + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' }, "field_no_map": [ "warehouse" diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 4bbf3de5940..9de088df0ee 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -458,7 +458,7 @@ "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "From Warehouse", + "label": "Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -467,11 +467,12 @@ "width": "100px" }, { + "depends_on": "eval:parent.is_internal_customer", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -748,7 +749,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 19:59:27.119856", + "modified": "2020-12-26 17:31:27.029803", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index faeeb578fe3..ec32b0f0440 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -384,7 +384,10 @@ $.extend(erpnext.item, { ' + __("Stock Levels") + ''); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, - item_code: frm.doc.name + item_code: frm.doc.name, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }); erpnext.item.item_dashboard.refresh(); }); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d07b3dc4fef..fcf7c2608ef 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -106,9 +106,9 @@ "item_tax_section_break", "taxes", "inspection_criteria", + "quality_inspection_template", "inspection_required_before_purchase", "inspection_required_before_delivery", - "quality_inspection_template", "manufacturing", "default_bom", "is_sub_contracted_item", @@ -814,7 +814,6 @@ "label": "Inspection Required before Delivery" }, { - "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)", "fieldname": "quality_inspection_template", "fieldtype": "Link", "label": "Quality Inspection Template", @@ -1069,7 +1068,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-08-07 14:24:58.384992", + "modified": "2021-01-25 20:49:50.222976", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1131,4 +1130,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 2729f4b15e2..e4db0480db0 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -15,5 +15,13 @@ frappe.ui.form.on("Item Price", { frm.set_df_property("bulk_import_help", "options", '' + __("Import in Bulk") + ''); + + frm.set_query('batch_no', function() { + return { + filters: { + 'item': frm.doc.item_code + } + } + }); } }); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 5f62381f8b3..83177b372ad 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,6 +18,7 @@ "price_list", "customer", "supplier", + "batch_no", "column_break_3", "buying", "selling", @@ -47,31 +48,41 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit" + "label": "Packing Unit", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "item_code.brand", @@ -79,19 +90,25 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags" + "options": "fa fa-tags", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list", @@ -100,7 +117,9 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -108,37 +127,49 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer" + "options": "Customer", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier" + "options": "Supplier", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -146,11 +177,15 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_rate", @@ -162,53 +197,80 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From" + "label": "Valid From", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days" + "label": "Lead Time in days", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Upto", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_24", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note" + "label": "Note", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference" + "label": "Reference", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-flag", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-06 22:31:32.943475", + "modified": "2020-12-08 18:12:15.395772", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bed5ea9ab66..e82a19b0dc0 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,7 +54,8 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier",]: + "supplier", + "batch_no"]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -68,7 +69,7 @@ class ItemPrice(Document): self.as_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,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index b24d621c317..c77b993167a 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2013-02-22 01:28:02", "doctype": "DocType", "document_type": "Document", @@ -29,6 +30,8 @@ "options": "Item", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -41,6 +44,8 @@ "print_width": "300px", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "120px" }, { @@ -50,7 +55,9 @@ "no_copy": 1, "options": "Purchase Invoice\nPurchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "receipt_document", @@ -59,25 +66,33 @@ "no_copy": 1, "options": "receipt_document_type", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, "label": "Qty", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rate", "fieldtype": "Currency", "label": "Rate", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amount", @@ -88,14 +103,19 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "applicable_charges", "fieldtype": "Currency", "in_list_view": 1, "label": "Applicable Charges", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only_depends_on": "eval:parent.distribute_charges_based_on != 'Distribute Manually'", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_item", @@ -104,22 +124,30 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions" + "label": "Accounting Dimensions", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -128,12 +156,15 @@ "fieldtype": "Check", "hidden": 1, "label": "Is Fixed Asset", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2021-01-25 23:09:23.322282", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 64331c7d578..4fcdb4c10cc 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -6,8 +6,11 @@ "engine": "InnoDB", "field_order": [ "expense_account", + "account_currency", + "exchange_rate", "description", "col_break3", + "base_amount", "amount" ], "fields": [ @@ -28,7 +31,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency", + "options": "account_currency", "reqd": 1 }, { @@ -38,13 +41,33 @@ "in_list_view": 1, "label": "Expense Account", "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", - "options": "Account", - "print_hide": 1 + "options": "Account" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Base Amount", + "options": "Company:company:default_currency", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-04 00:22:14.373312", + "modified": "2020-12-26 01:07:23.233604", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 5de13525183..1abbc35334f 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; frappe.provide("erpnext.stock"); @@ -29,20 +30,9 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ this.frm.add_fetch("receipt_document", "supplier", "supplier"); this.frm.add_fetch("receipt_document", "posting_date", "posting_date"); this.frm.add_fetch("receipt_document", "base_grand_total", "grand_total"); - - this.frm.set_query("expense_account", "taxes", function() { - return { - query: "erpnext.controllers.queries.tax_account_query", - filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], - "company": me.frm.doc.company - } - }; - }); - }, - refresh: function(frm) { + refresh: function() { var help_content = `

@@ -72,6 +62,11 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
`; set_field_options("landed_cost_help", help_content); + + if (this.frm.doc.company) { + let company_currency = frappe.get_doc(":Company", this.frm.doc.company).default_currency; + this.frm.set_currency_labels(["total_taxes_and_charges"], company_currency); + } }, get_items_from_purchase_receipts: function() { @@ -97,34 +92,36 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ set_total_taxes_and_charges: function() { var total_taxes_and_charges = 0.0; $.each(this.frm.doc.taxes || [], function(i, d) { - total_taxes_and_charges += flt(d.amount) + total_taxes_and_charges += flt(d.base_amount); }); - cur_frm.set_value("total_taxes_and_charges", total_taxes_and_charges); + this.frm.set_value("total_taxes_and_charges", total_taxes_and_charges); }, set_applicable_charges_for_item: function() { var me = this; if(this.frm.doc.taxes.length) { - var total_item_cost = 0.0; var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase(); - $.each(this.frm.doc.items || [], function(i, d) { - total_item_cost += flt(d[based_on]) - }); - var total_charges = 0.0; - $.each(this.frm.doc.items || [], function(i, item) { - item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) - item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) - total_charges += item.applicable_charges - }); + if (based_on != 'distribute manually') { + $.each(this.frm.doc.items || [], function(i, d) { + total_item_cost += flt(d[based_on]) + }); - if (total_charges != this.frm.doc.total_taxes_and_charges){ - var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) - this.frm.doc.items.slice(-1)[0].applicable_charges += diff + var total_charges = 0.0; + $.each(this.frm.doc.items || [], function(i, item) { + item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) + item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) + total_charges += item.applicable_charges + }); + + if (total_charges != this.frm.doc.total_taxes_and_charges){ + var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) + this.frm.doc.items.slice(-1)[0].applicable_charges += diff + } + refresh_field("items"); } - refresh_field("items"); } }, distribute_charges_based_on: function (frm) { @@ -134,7 +131,16 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ items_remove: () => { this.trigger('set_applicable_charges_for_item'); } - }); cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher); + +frappe.ui.form.on('Landed Cost Taxes and Charges', { + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); + }, + + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + } +}); diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 01492807def..059f925184b 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2014-07-11 11:33:42.547339", "doctype": "DocType", @@ -7,6 +8,9 @@ "field_order": [ "naming_series", "company", + "column_break_2", + "posting_date", + "section_break_5", "purchase_receipts", "purchase_receipt_items", "get_items_from_purchase_receipts", @@ -30,7 +34,9 @@ "options": "MAT-LCV-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -40,24 +46,32 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipts", "fieldtype": "Table", "label": "Purchase Receipts", "options": "Landed Cost Purchase Receipt", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_items", "fieldtype": "Section Break", - "label": "Purchase Receipt Items" + "label": "Purchase Receipt Items", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "get_items_from_purchase_receipts", "fieldtype": "Button", - "label": "Get Items From Purchase Receipts" + "label": "Get Items From Purchase Receipts", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items", @@ -65,42 +79,56 @@ "label": "Purchase Receipt Items", "no_copy": 1, "options": "Landed Cost Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break1", "fieldtype": "Section Break", - "label": "Applicable Charges" + "label": "Applicable Charges", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", "fieldtype": "Table", "label": "Taxes and Charges", "options": "Landed Cost Taxes and Charges", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", - "label": "Total Taxes and Charges", + "label": "Total Taxes and Charges (Company Currency)", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "distribute_charges_based_on", "fieldtype": "Select", "label": "Distribute Charges Based On", - "options": "Qty\nAmount", - "reqd": 1 + "options": "Qty\nAmount\nDistribute Manually", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -109,21 +137,51 @@ "no_copy": 1, "options": "Landed Cost Voucher", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "landed_cost_help", "fieldtype": "HTML", - "label": "Landed Cost Help" + "label": "Landed Cost Help", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "icon-usd", + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-11-21 15:34:10.846093", + "links": [], + "modified": "2021-01-25 23:07:30.468423", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 9ec6b8946cc..69a8bf19d34 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,6 +9,7 @@ from frappe.model.meta import get_field_precision from frappe.model.document import Document from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.accounts.doctype.account.account import get_account_currency +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals class LandedCostVoucher(Document): def get_items_from_purchase_receipts(self): @@ -39,13 +40,15 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() + self.validate_purchase_receipts() + init_landed_taxes_and_totals(self) + self.set_total_taxes_and_charges() if not self.get("items"): self.get_items_from_purchase_receipts() - else: - self.validate_applicable_charges_for_item() - self.validate_purchase_receipts() - self.validate_expense_accounts() - self.set_total_taxes_and_charges() + + self.set_applicable_charges_on_item() + self.validate_applicable_charges_for_item() + def check_mandatory(self): if not self.get("purchase_receipts"): @@ -73,21 +76,37 @@ class LandedCostVoucher(Document): frappe.throw(_("Row {0}: Cost center is required for an item {1}") .format(item.idx, item.item_code)) - def validate_expense_accounts(self): - company_currency = erpnext.get_company_currency(self.company) - for account in self.taxes: - if get_account_currency(account.expense_account) != company_currency: - frappe.throw(_("Row {}: Expense account currency should be same as company's default currency.").format(account.idx) - + _("Please select expense account with account currency as {}.").format(frappe.bold(company_currency)), - title=_("Invalid Account Currency")) - def set_total_taxes_and_charges(self): - self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")]) + self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + + def set_applicable_charges_on_item(self): + if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': + total_item_cost = 0.0 + total_charges = 0.0 + item_count = 0 + based_on_field = frappe.scrub(self.distribute_charges_based_on) + + for item in self.get('items'): + total_item_cost += item.get(based_on_field) + + for item in self.get('items'): + item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + item.precision('applicable_charges')) + total_charges += item.applicable_charges + item_count += 1 + + if total_charges != self.total_taxes_and_charges: + diff = self.total_taxes_and_charges - total_charges + self.get('items')[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - total = sum([flt(d.get(based_on)) for d in self.get("items")]) + if based_on != 'distribute manually': + total = sum([flt(d.get(based_on)) for d in self.get("items")]) + else: + # consider for proportion while distributing manually + total = sum([flt(d.get('applicable_charges')) for d in self.get("items")]) if not total: frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) @@ -153,13 +172,13 @@ class LandedCostVoucher(Document): docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, 'item_code': item.item_code }, fields=['name', 'docstatus']) if not docs or len(docs) != item.qty: - frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document) - + _('Please create or link {0} Assets with respective document.').format(item.qty)) + frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format( + item.receipt_document, item.qty)) if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.') - .format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code))) + frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format( + item.receipt_document, item.item_code, item.receipt_document_type)) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index b97213e4fba..144101c67d7 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -10,6 +10,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ import get_gl_entries, test_records as pr_test_records, make_purchase_receipt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.accounts.doctype.account.test_account import create_account class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): @@ -162,8 +163,8 @@ class TestLandedCostVoucher(unittest.TestCase): lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) - self.assertEqual(lcv.items[0].applicable_charges, 41.07) - self.assertEqual(lcv.items[2].applicable_charges, 41.08) + self.assertEqual(flt(lcv.items[0].applicable_charges, 2), 41.07) + self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", @@ -206,6 +207,46 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) + def test_multi_currency_lcv(self): + ## Create USD Shipping charges_account + usd_shipping = create_account(account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", + account_currency="USD") + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Stores - TCP1") + pr.submit() + + lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", + receipt_document=pr.name, charges=100, do_not_save=True) + + lcv.append("taxes", { + "description": "Shipping Charges", + "expense_account": usd_shipping, + "amount": 10 + }) + + lcv.save() + lcv.submit() + pr.load_from_db() + + # Considering exchange rate from USD to INR as 62.9 + self.assertEqual(lcv.total_taxes_and_charges, 729) + self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) + + gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], + filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + + expected_gl_entries = { + "Shipping Charges USD - TCP1": [629, 10], + "Expenses Included In Valuation - TCP1": [100, 100] + } + + for entry in gl_entries: + amounts = expected_gl_entries.get(entry.account) + self.assertEqual(entry.credit, amounts[0]) + self.assertEqual(entry.credit_in_account_currency, amounts[1]) + def make_landed_cost_voucher(** args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 0d7095875c6..de7a3d05bf5 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,9 +1,10 @@ frappe.listview_settings['Material Request'] = { add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { - if(doc.status=="Stopped") { + var precision = frappe.defaults.get_default("float_precision"); + if (doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; - } else if(doc.transfer_status && doc.docstatus != 2) { + } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { return [__("Not Started"), "orange"]; } else if (doc.transfer_status == "In Transit") { @@ -11,14 +12,14 @@ frappe.listview_settings['Material Request'] = { } else if (doc.transfer_status == "Completed") { return [__("Completed"), "green"]; } - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) { return [__("Partially ordered"), "yellow", "per_ordered,<,100"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) { - if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) { + if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) { return [__("Partially Received"), "yellow", "per_received,<,100"]; - } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) == 100) { + } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { return [__("Received"), "green", "per_received,=,100"]; } else if (doc.material_request_type == "Purchase") { return [__("Ordered"), "green", "per_ordered,=,100"]; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index d998729c479..57cc3504a90 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -216,6 +216,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }); }, + apply_putaway_rule: function() { + if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + } + }); // for backward compatibility: combine new and previous states diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 5bb3095708f..32d349f3031 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,6 +21,7 @@ "posting_date", "posting_time", "set_posting_time", + "apply_putaway_rule", "is_return", "return_against", "section_addresses", @@ -47,6 +48,7 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", "supplier_warehouse", "items_section", @@ -114,6 +116,7 @@ "per_returned", "is_internal_supplier", "inter_company_reference", + "represents_company", "subscription_detail", "auto_repeat", "printing_settings", @@ -1086,7 +1089,9 @@ "fieldname": "inter_company_reference", "fieldtype": "Link", "label": "Inter Company Reference", + "no_copy": 1, "options": "Delivery Note", + "print_hide": 1, "read_only": 1 }, { @@ -1106,6 +1111,12 @@ "label": "Billing Address", "read_only": 1 }, + { + "default": "0", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" + }, { "depends_on": "eval:!doc.__islocal", "fieldname": "per_returned", @@ -1114,13 +1125,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_supplier", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-11-30 12:54:23.278500", + "modified": "2020-12-26 20:49:39.106049", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f833fc75c04..550c849c5d1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -83,6 +83,12 @@ class PurchaseReceipt(BuyingController): } ]) + def before_validate(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + + if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): + apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() @@ -103,6 +109,7 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + def validate_cwip_accounts(self): for item in self.get('items'): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): @@ -281,12 +288,15 @@ class PurchaseReceipt(BuyingController): # Amount added through landed-cost-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): + account_currency = get_account_currency(account) gl_entries.append(self.get_gl_dict({ "account": account, + "account_currency": account_currency, "against": warehouse_account[d.warehouse]["account"], "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": d.project }, item=d)) @@ -408,7 +418,7 @@ class PurchaseReceipt(BuyingController): if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_entries) def get_asset_gl_entry(self, gl_entries): @@ -721,7 +731,13 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + + #Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': + based_on_field = 'amount' + else: + based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + total_item_cost = 0 for item in landed_cost_voucher_doc.items: @@ -731,9 +747,16 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \ + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, { + "amount": 0.0, + "base_amount": 0.0 + }) + + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \ account.amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \ + account.base_amount * item.get(based_on_field) / total_item_cost + return item_account_wise_cost diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e7a58b7aeaa..37e0c7f3442 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1014,6 +1014,7 @@ def make_purchase_receipt(**args): pr.currency = args.currency or "INR" pr.is_return = args.is_return pr.return_against = args.return_against + pr.apply_putaway_rule = args.apply_putaway_rule qty = args.qty or 5 received_qty = args.received_qty or qty rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) @@ -1029,6 +1030,7 @@ def make_purchase_receipt(**args): "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 871b255b06a..e99119202e7 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -76,6 +76,8 @@ "purchase_order_item", "material_request_item", "purchase_receipt_item", + "delivery_note_item", + "putaway_rule", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -818,11 +820,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -839,6 +842,15 @@ "fieldname": "image_column", "fieldtype": "Column Break" }, + { + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "tracking_section", "fieldtype": "Section Break" @@ -861,12 +873,20 @@ "fieldtype": "Float", "label": "Received Qty in Stock UOM", "print_hide": 1 + }, + { + "fieldname": "delivery_note_item", + "fieldtype": "Data", + "label": "Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-07 10:00:38.204294", + "modified": "2020-12-26 16:50:56.479347", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js new file mode 100644 index 00000000000..e0569206ef9 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -0,0 +1,43 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Putaway Rule', { + setup: function(frm) { + frm.set_query("warehouse", function() { + return { + "filters": { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + }, + + uom: function(frm) { + if (frm.doc.item_code && frm.doc.uom) { + return frm.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { + item_code: frm.doc.item_code, + uom: frm.doc.uom + }, + callback: function(r) { + if (!r.exc) { + let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); + frm.set_value('conversion_factor', r.message.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + } + }); + } + }, + + capacity: function(frm) { + let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json new file mode 100644 index 00000000000..a003f4986f9 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "PUT-.####", + "creation": "2020-11-09 11:39:46.489501", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable", + "item_code", + "item_name", + "warehouse", + "priority", + "col_break_capacity", + "company", + "capacity", + "uom", + "conversion_factor", + "stock_uom", + "stock_capacity" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "col_break_capacity", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capacity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Capacity", + "reqd": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Priority" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "no_copy": 1, + "options": "UOM" + }, + { + "fieldname": "stock_capacity", + "fieldtype": "Float", + "label": "Capacity in Stock UOM", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "no_copy": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-25 20:39:19.973437", + "modified_by": "Administrator", + "module": "Stock", + "name": "Putaway Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "item_code", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py new file mode 100644 index 00000000000..ea26caced04 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +import json +from collections import defaultdict +from six import string_types +from frappe import _ +from frappe.utils import flt, floor, nowdate, cint +from frappe.model.document import Document +from erpnext.stock.utils import get_stock_balance +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +class PutawayRule(Document): + def validate(self): + self.validate_duplicate_rule() + self.validate_warehouse_and_company() + self.validate_capacity() + self.validate_priority() + self.set_stock_capacity() + + def validate_duplicate_rule(self): + existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + if existing_rule and existing_rule != self.name: + frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") + .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), + title=_("Duplicate")) + + def validate_priority(self): + if self.priority < 1: + frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority")) + + def validate_warehouse_and_company(self): + company = frappe.db.get_value("Warehouse", self.warehouse, "company") + if company != self.company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.") + .format(frappe.bold(self.warehouse), frappe.bold(self.company)), + title=_("Invalid Warehouse")) + + def validate_capacity(self): + stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") + balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) + + if flt(self.stock_capacity) < flt(balance_qty): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") + .format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity")) + + if not self.capacity: + frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + + def set_stock_capacity(self): + self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + +@frappe.whitelist() +def get_available_putaway_capacity(rule): + stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, + ["stock_capacity", "item_code", "warehouse"]) + balance_qty = get_stock_balance(item_code, warehouse, nowdate()) + free_space = flt(stock_capacity) - flt(balance_qty) + return free_space if free_space > 0 else 0 + +@frappe.whitelist() +def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): + """ Applies Putaway Rule on line items. + + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls + """ + if isinstance(items, string_types): + items = json.loads(items) + + items_not_accomodated, updated_table = [], [] + item_wise_rules = defaultdict(list) + + for item in items: + if isinstance(item, dict): + item = frappe._dict(item) + + source_warehouse = item.get("s_warehouse") + serial_nos = get_serial_nos(item.get("serial_no")) + item.conversion_factor = flt(item.conversion_factor) or 1.0 + pending_qty, item_code = flt(item.qty), item.item_code + pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) + continue + + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) + + if not rules: + warehouse = source_warehouse or item.warehouse + if at_capacity: + # rules available, but no free space + items_not_accomodated.append([item_code, pending_qty]) + else: + updated_table = add_row(item, pending_qty, warehouse, updated_table) + continue + + # maintain item/item-warehouse wise rules, to handle if item is entered twice + # in the table, due to different price, etc. + key = item_code + if doctype == "Stock Entry" and purpose == "Material Transfer" and source_warehouse: + key = (item_code, source_warehouse) + + if not item_wise_rules[key]: + item_wise_rules[key] = rules + + for rule in item_wise_rules[key]: + if pending_stock_qty > 0 and rule.free_space: + stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + qty_to_allocate = stock_qty_to_allocate / item.conversion_factor + + if uom_must_be_whole_number: + qty_to_allocate = floor(qty_to_allocate) + stock_qty_to_allocate = qty_to_allocate * item.conversion_factor + + if not qty_to_allocate: break + + updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, + rule.name, serial_nos=serial_nos) + + pending_stock_qty -= stock_qty_to_allocate + pending_qty -= qty_to_allocate + rule["free_space"] -= stock_qty_to_allocate + + if not pending_stock_qty > 0: break + + # if pending qty after applying all rules, add row without warehouse + if pending_stock_qty > 0: + items_not_accomodated.append([item.item_code, pending_qty]) + + if items_not_accomodated: + show_unassigned_items_message(items_not_accomodated) + + items[:] = updated_table if updated_table else items # modify items table + + if sync and json.loads(sync): # sync with client side + return items + +def get_ordered_putaway_rules(item_code, company, source_warehouse=None): + """Returns an ordered list of putaway rules to apply on an item.""" + filters = { + "item_code": item_code, + "company": company, + "disable": 0 + } + if source_warehouse: + filters.update({"warehouse": ["!=", source_warehouse]}) + + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + filters=filters, + order_by="priority asc, capacity desc") + + if not rules: + return False, None + + vacant_rules = [] + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.stock_capacity) - flt(balance_qty) + if free_space > 0: + rule["free_space"] = free_space + vacant_rules.append(rule) + + if not vacant_rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + return True, None + + vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + + return False, vacant_rules + +def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 + new_updated_table_row.name = None + new_updated_table_row.qty = to_allocate + + if item.doctype == "Stock Entry Detail": + new_updated_table_row.t_warehouse = warehouse + new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + else: + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + new_updated_table_row.warehouse = warehouse + new_updated_table_row.rejected_qty = 0 + new_updated_table_row.received_qty = to_allocate + + if rule: + new_updated_table_row.putaway_rule = rule + if serial_nos: + new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) + + updated_table.append(new_updated_table_row) + return updated_table + +def show_unassigned_items_message(items_not_accomodated): + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

" + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
{0}{1}
+ """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + +def get_serial_nos_to_allocate(serial_nos, to_allocate): + if serial_nos: + allocated_serial_nos = serial_nos[0: cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" + else: return "" \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js new file mode 100644 index 00000000000..725e91ee8d9 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -0,0 +1,18 @@ +frappe.listview_settings['Putaway Rule'] = { + add_fields: ["disable"], + get_indicator: (doc) => { + if (doc.disable) { + return [__("Disabled"), "darkgrey", "disable,=,1"]; + } else { + return [__("Active"), "blue", "disable,=,0"]; + } + }, + + reports: [ + { + name: 'Warehouse Capacity Summary', + report_type: 'Page', + route: 'warehouse-capacity-summary' + } + ] +}; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py new file mode 100644 index 00000000000..86f7dc3e084 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.batch.test_batch import make_new_batch +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + +class TestPutawayRule(unittest.TestCase): + def setUp(self): + if not frappe.db.exists("Item", "_Rice"): + make_item("_Rice", { + 'is_stock_item': 1, + 'has_batch_no' : 1, + 'create_new_batch': 1, + 'stock_uom': 'Kg' + }) + + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): + create_warehouse("Rack 1") + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): + create_warehouse("Rack 2") + + self.warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + self.warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + if not frappe.db.exists("UOM", "Bag"): + new_uom = frappe.new_doc("UOM") + new_uom.uom_name = "Bag" + new_uom.save() + + def test_putaway_rules_priority(self): + """Test if rule is applied by priority, irrespective of free space.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, + uom="Kg", priority=2) + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, self.warehouse_2) + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_same_priority(self): + """Test if rule with more free space is applied, + among two rules with same priority and capacity.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, + uom="Kg") + + # out of 500 kg capacity, occupy 100 kg in warehouse_1 + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) + + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 500) + # warehouse_2 has 500 kg free space, it is given priority + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 200) + # warehouse_1 has 400 kg free space, it is given less priority + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + + stock_receipt.cancel() + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_insufficient_capacity(self): + """Test if qty exceeding capacity, is handled.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + # total 300 assigned, 50 unassigned + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom(self): + """Test rules applied on uom other than stock uom.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 3000) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, + uom="Bag") + self.assertEqual(rule_2.stock_capacity, 4000) + + # populate 'Rack 1' with 1 Bag, making the free space 2 Bags + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) + + pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 4) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 2) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + + stock_receipt.cancel() + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom_whole_uom(self): + """Test if whole UOMs are handled.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) + + # Putaway Rule in different UOM + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 1000) + # Putaway Rule in Stock UOM + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) + self.assertEqual(rule_2.stock_capacity, 500) + # total capacity is 1500 Kg + + pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + # leftover space was for 500 kg (0.5 Bag) + # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_reoccurring_item(self): + """Test rules on same item entered multiple times with different rate.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + # total capacity is 200 Kg + + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, + do_not_submit=1) + pr.append("items", { + "item_code": "_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }) # same item entered again in PR but with different rate + pr.save() + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 100) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + # same rule applied to second item row + # with previous assignment considered + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + self.assertEqual(pr.items[1].putaway_rule, rule_1.name) + + pr.delete() + rule_1.delete() + + def test_validate_over_receipt_in_warehouse(self): + """Test if overreceipt is blocked in the presence of putaway rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + + # force overreceipt and disable apply putaway rule in PR + pr.items[0].qty = 300 + pr.items[0].stock_qty = 300 + pr.apply_putaway_rule = 0 + self.assertRaises(frappe.ValidationError, pr.save) + + pr.delete() + rule_1.delete() + + def test_putaway_rule_on_stock_entry_material_transfer(self): + """Test if source warehouse is considered while applying rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # higher priority + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg", priority=2) + + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided + # even though it has more free space and higher priority + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): + """Test if reoccuring item is correctly considered.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, + uom="Kg", priority=2) + + # create SE with first row having source warehouse as Rack 2 + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + # Add rows with source warehouse as Rack 1 + stock_entry.extend("items", [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100 + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200 + } + ]) + + stock_entry.save() + + # since source warehouse was Rack 2, exclude rule_2 + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 200) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + + # since source warehouse was Rack 1, exclude rule_1 even though it has + # higher priority + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 100) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + + self.assertEqual(stock_entry.items[2].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[2].qty, 200) + self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): + """Test if batch and serial items are split correctly.""" + if not frappe.db.exists("Item", "Water Bottle"): + make_item("Water Bottle", { + "is_stock_item": 1, + "has_batch_no" : 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos" + }) + + rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, + uom="Nos") + rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, + uom="Nos") + + make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") + + pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1) + pr.items[0].batch_no = "BOTTL-BATCH-1" + pr.save() + pr.submit() + + serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = [d.name for d in serial_nos] + + stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, + target="Finished Goods - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_save=1) + stock_entry.items[0].batch_no = "BOTTL-BATCH-1" + stock_entry.items[0].serial_no = "\n".join(serial_nos) + stock_entry.save() + + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 3) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3])) + self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1") + + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 2) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) + self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + + stock_entry.delete() + pr.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_receipt(self): + """Test if rules are applied in Stock Entry of type Receipt.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # more capacity + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg") + + stock_entry = make_stock_entry(item_code="_Rice", qty=100, + target="_Test Warehouse - _TC", purpose="Material Receipt", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry_item.qty, 100) + self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + +def create_putaway_rule(**args): + args = frappe._dict(args) + putaway = frappe.new_doc("Putaway Rule") + + putaway.disable = args.disable or 0 + putaway.company = args.company or "_Test Company" + putaway.item_code = args.item or args.item_code or "_Test Item" + putaway.warehouse = args.warehouse + putaway.priority = args.priority or 1 + putaway.capacity = args.capacity or 1 + putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") + putaway.uom = args.uom or putaway.stock_uom + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + + if not args.do_not_save: + putaway.save() + + return putaway \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 2fc4f96d128..274bbc28b13 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -3,8 +3,18 @@ frappe.provide("erpnext.stock"); frappe.provide("erpnext.accounts.dimensions"); +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; + frappe.ui.form.on('Stock Entry', { setup: function(frm) { + frm.set_indicator_formatter('item_code', function(doc) { + if (!doc.s_warehouse) { + return 'blue'; + } else { + return (doc.qty<=doc.actual_qty) ? 'green' : 'orange'; + } + }); + frm.set_query('work_order', function() { return { filters: [ @@ -87,15 +97,6 @@ frappe.ui.form.on('Stock Entry', { } }); - frm.set_query("expense_account", "additional_costs", function() { - return { - query: "erpnext.controllers.queries.tax_account_query", - filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], - "company": frm.doc.company - } - }; - }); frm.add_fetch("bom_no", "inspection_required", "inspection_required"); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); @@ -550,7 +551,7 @@ frappe.ui.form.on('Stock Entry', { calculate_total_additional_costs: function(frm) { const total_additional_costs = frappe.utils.sum( - (frm.doc.additional_costs || []).map(function(c) { return flt(c.amount); }) + (frm.doc.additional_costs || []).map(function(c) { return flt(c.base_amount); }) ); frm.set_value("total_additional_costs", @@ -583,8 +584,12 @@ frappe.ui.form.on('Stock Entry', { } }); } + }, + + apply_putaway_rule: function (frm) { + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); } -}) +}); frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { @@ -725,8 +730,18 @@ var validate_sample_quantity = function(frm, cdt, cdn) { }; frappe.ui.form.on('Landed Cost Taxes and Charges', { - amount: function(frm) { - frm.events.calculate_amount(frm); + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + + // Adding this check because same table in used in LCV + // This causes an error if you try to post an LCV immediately after a Stock Entry + if (frm.doc.doctype == 'Stock Entry') { + frm.events.calculate_amount(frm); + } + }, + + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); } }); @@ -774,15 +789,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } } - this.frm.set_indicator_formatter('item_code', - function(doc) { - if (!doc.s_warehouse) { - return 'blue'; - } else { - return (doc.qty<=doc.actual_qty) ? "green" : "orange" - } - }) - this.frm.add_fetch("purchase_order", "supplier", "supplier"); frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 5aed08102c7..98c047a09ed 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,6 +27,7 @@ "set_posting_time", "inspection_required", "from_bom", + "apply_putaway_rule", "sb1", "bom_no", "fg_completed_qty", @@ -640,6 +641,13 @@ "fieldtype": "Check", "label": "Add to Transit", "no_copy": 1 + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], doc.purpose)", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-file-text", @@ -647,7 +655,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-09 12:59:02.508943", + "modified": "2020-12-09 14:58:13.267321", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4782a9df0f1..d623d5c7589 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -19,6 +19,7 @@ from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError from erpnext.accounts.general_ledger import process_gl_map +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals import json from six import string_types, itervalues, iteritems @@ -42,6 +43,14 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) + def before_validate(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + if self.get("items") and apply_rule: + apply_putaway_rule(self.doctype, self.get("items"), self.company, + purpose=self.purpose) + def validate(self): self.pro_doc = frappe._dict() if self.work_order: @@ -79,6 +88,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount() + self.validate_putaway_capacity() def on_submit(self): self.update_stock_ledger() @@ -186,7 +196,7 @@ class StockEntry(StockController): and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where @@ -436,6 +446,7 @@ class StockEntry(StockController): def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate) + init_landed_taxes_and_totals(self) self.distribute_additional_costs() self.update_valuation_rate() self.set_total_incoming_outgoing_value() @@ -524,7 +535,7 @@ class StockEntry(StockController): if not any([d.item_code for d in self.items if d.t_warehouse]): self.additional_costs = [] - self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")]) + self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")]) if self.purpose in ("Repack", "Manufacture"): incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item]) @@ -764,13 +775,19 @@ class StockEntry(StockController): for d in self.get("items"): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { + "amount": 0.0, + "base_amount": 0.0 + }) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ flt(t.amount * multiply_based_on) / divide_based_on + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + flt(t.base_amount * multiply_based_on) / divide_based_on + if item_account_wise_additional_cost: for d in self.get("items"): for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): @@ -781,7 +798,8 @@ class StockEntry(StockController): "against": d.expense_account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": amount + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]) }, item=d)) gl_entries.append(self.get_gl_dict({ @@ -789,7 +807,7 @@ class StockEntry(StockController): "against": account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount # put it as negative credit instead of debit purposefully + "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully }, item=d)) return process_gl_map(gl_entries) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b78c6be983f..b12a8547fea 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -53,6 +53,8 @@ def make_stock_entry(**args): args.target = args.to_warehouse if args.item_code: args.item = args.item_code + if args.apply_putaway_rule: + s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): if '.' in args.qty: @@ -118,7 +120,8 @@ def make_stock_entry(**args): "t_warehouse": args.target, "qty": args.qty, "basic_rate": args.rate or args.basic_rate, - "conversion_factor": 1.0, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, 'batch_no': args.batch_no, 'cost_center': args.cost_center, diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index b78ae6d79b3..988ae929691 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -65,6 +66,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "putaway_rule", "column_break_51", "reference_purchase_receipt", "quality_inspection" @@ -495,6 +497,16 @@ "fieldtype": "Check", "label": "Set Basic Rate Manually" }, + { + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], parent.purpose)", + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "quantity_section", "fieldtype": "Section Break", @@ -526,7 +538,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-23 17:55:03.384138", + "modified": "2020-12-30 15:00:44.489442", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5b40292ea8f..f0a90f9754b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -30,6 +30,7 @@ class StockReconciliation(StockController): self.validate_data() self.validate_expense_account() self.set_total_qty_and_amount() + self.validate_putaway_capacity() if self._action=="submit": self.make_batches('warehouse') diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 3ff396ba77e..84af57b48dd 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -16,6 +16,7 @@ "action_if_quality_inspection_is_not_submitted", "show_barcode_field", "clean_description_html", + "disable_serial_no_and_batch_selector", "section_break_7", "auto_insert_price_list_rate_if_missing", "allow_negative_stock", @@ -227,6 +228,12 @@ "fieldname": "control_historical_stock_transactions_section", "fieldtype": "Section Break", "label": "Control Historical Stock Transactions" + }, + { + "default": "0", + "fieldname": "disable_serial_no_and_batch_selector", + "fieldtype": "Check", + "label": "Disable Serial No And Batch Selector" } ], "icon": "icon-cog", @@ -234,7 +241,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-29 12:53:31.162247", + "modified": "2021-01-18 13:15:38.352796", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bf45251c9d8..dfe8fea67b9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -674,6 +674,8 @@ def get_item_price(args, item_code, ignore_party=False): and price_list=%(price_list)s and ifnull(uom, '') in ('', %(uom)s)""" + conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + if not ignore_party: if args.get("customer"): conditions += " and customer=%(customer)s" @@ -692,7 +694,7 @@ def get_item_price(args, item_code, ignore_party=False): return frappe.db.sql(""" select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ @@ -711,6 +713,7 @@ def get_price_list_rate_for(args, item_code): "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), "posting_date": args.get('posting_date'), + "batch_no": args.get('batch_no') } item_price_data = 0 diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js new file mode 100644 index 00000000000..f3f61963a88 --- /dev/null +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -0,0 +1,62 @@ +let document_list = ['Landed Cost Voucher', 'Stock Entry']; + +document_list.forEach((doctype) => { + frappe.ui.form.on(doctype, { + refresh: function(frm) { + let tax_field = frm.doc.doctype == 'Landed Cost Voucher' ? 'taxes' : 'additional_costs'; + frm.set_query("expense_account", tax_field, function() { + return { + filters: { + "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "company": frm.doc.company + } + }; + }); + }, + + set_account_currency: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.expense_account) { + frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { + frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); + frm.events.set_exchange_rate(frm, cdt, cdn); + }); + } + }, + + set_exchange_rate: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + + if (row.account_currency == company_currency) { + row.exchange_rate = 1; + frm.set_df_property('taxes', 'hidden', 1, row.name, 'exchange_rate'); + } else if (!row.exchange_rate || row.exchange_rate == 1) { + frm.set_df_property('taxes', 'hidden', 0, row.name, 'exchange_rate'); + frappe.call({ + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", + args: { + posting_date: frm.doc.posting_date, + account: row.expense_account, + account_currency: row.account_currency, + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); + } + } + }); + } + + frm.refresh_field('taxes'); + }, + + set_base_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, "base_amount", + flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); + } + }); +}); + diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index da21c6bc64f..bddffd465e6 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { frappe.require('assets/js/item-dashboard.min.js', function() { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }) page.item_dashboard.before_refresh = function() { diff --git a/erpnext/stock/page/warehouse_capacity_summary/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html new file mode 100644 index 00000000000..90112c78a83 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -0,0 +1,40 @@ +{% for d in data %} +
+
+ + +
+ {{ d.stock_capacity }} +
+
+ {{ d.actual_qty }} +
+
+
+
+
+
+
+
+ {{ d.percent_occupied }}% +
+ {% if can_write %} +
+
+ {% endif %} +
+
+{% endfor %} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js new file mode 100644 index 00000000000..b610e7dd587 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -0,0 +1,120 @@ +frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Warehouse Capacity Summary', + single_column: true + }); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.start = 0; + + page.company_field = page.add_field({ + fieldname: 'company', + label: __('Company'), + fieldtype: 'Link', + options: 'Company', + reqd: 1, + default: frappe.defaults.get_default("company"), + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.warehouse_field = page.add_field({ + fieldname: 'warehouse', + label: __('Warehouse'), + fieldtype: 'Link', + options: 'Warehouse', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.item_field = page.add_field({ + fieldname: 'item_code', + label: __('Item'), + fieldtype: 'Link', + options: 'Item', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.parent_warehouse_field = page.add_field({ + fieldname: 'parent_warehouse', + label: __('Parent Warehouse'), + fieldtype: 'Link', + options: 'Warehouse', + get_query: function() { + return { + filters: { + "is_group": 1 + } + }; + }, + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.sort_selector = new frappe.ui.SortSelector({ + parent: page.wrapper.find('.page-form'), + args: { + sort_by: 'stock_capacity', + sort_order: 'desc', + options: [ + {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, + {fieldname: 'percent_occupied', label: __('% Occupied')}, + {fieldname: 'actual_qty', label: __('Balance Qty (Stock ')} + ] + }, + change: function(sort_by, sort_order) { + page.capacity_dashboard.sort_by = sort_by; + page.capacity_dashboard.sort_order = sort_order; + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + frappe.require('assets/js/item-dashboard.min.js', function() { + $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + + page.capacity_dashboard = new erpnext.stock.ItemDashboard({ + page_name: "warehouse-capacity-summary", + page_length: 10, + parent: page.main, + sort_by: 'stock_capacity', + sort_order: 'desc', + method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', + template: 'warehouse_capacity_summary' + }); + + page.capacity_dashboard.before_refresh = function() { + this.item_code = page.item_field.get_value(); + this.warehouse = page.warehouse_field.get_value(); + this.parent_warehouse = page.parent_warehouse_field.get_value(); + this.company = page.company_field.get_value(); + }; + + page.capacity_dashboard.refresh(); + + let setup_click = function(doctype) { + page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { + var name = $(this).attr('data-name'); + var field = page[doctype.toLowerCase() + '_field']; + if (field.get_value()===name) { + frappe.set_route('Form', doctype, name); + } else { + field.set_input(name); + page.capacity_dashboard.refresh(); + } + }); + }; + + setup_click('Item'); + setup_click('Warehouse'); + }); +}; \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json new file mode 100644 index 00000000000..a6e5b45332b --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-11-25 12:07:54.056208", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-11-25 11:07:54.056208", + "modified_by": "Administrator", + "module": "Stock", + "name": "warehouse-capacity-summary", + "owner": "Administrator", + "page_name": "Warehouse Capacity Summary", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Warehouse Capacity Summary" +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html new file mode 100644 index 00000000000..acaf180a903 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -0,0 +1,19 @@ +
+
+
+ Warehouse +
+
+ Item +
+
+ Stock Capacity +
+
+ Balance Stock Qty +
+
+ % Occupied +
+
+
\ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5b9ada0ee56..2b2a7a202dd 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -41,7 +41,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) - + args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) @@ -65,7 +65,7 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False): if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - + distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] i = 0 @@ -80,7 +80,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat for item_wh, new_sle in iteritems(obj.new_items): if item_wh not in distinct_item_warehouses: args.append(new_sle) - + i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -127,7 +127,7 @@ class update_entries_after(object): self.initialize_previous_data(self.args) self.build() - + def get_precision(self): company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency") self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), @@ -213,13 +213,13 @@ class update_entries_after(object): # includes current entry! args = self.data[self.args.warehouse].previous_sle \ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse}) - + return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name) - + if not dependant_sle: return elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: @@ -251,7 +251,7 @@ class update_entries_after(object): # Get dynamic incoming/outgoing rate self.get_dynamic_incoming_outgoing_rate(sle) - + if sle.serial_no: self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -329,7 +329,7 @@ class update_entries_after(object): rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - rate_field = "valuation_rate" + rate_field = "valuation_rate" else: rate_field = "incoming_rate" @@ -344,7 +344,7 @@ class update_entries_after(object): ref_doctype = "Packed Item" else: ref_doctype = "Purchase Receipt Item Supplied" - + rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, rate_field) @@ -374,7 +374,7 @@ class update_entries_after(object): stock_entry.db_update() for d in stock_entry.items: d.db_update() - + def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): # Update item's incoming rate on transaction item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code") @@ -487,7 +487,6 @@ class update_entries_after(object): self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: self.wh_data.valuation_rate = sle.outgoing_rate - else: if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate: self.wh_data.valuation_rate = sle.outgoing_rate @@ -631,7 +630,7 @@ class update_entries_after(object): frappe.throw(message, NegativeStockError, title='Insufficient Stock') else: raise NegativeStockError(message) - + def update_bin(self): # update bin for each warehouse for warehouse, data in iteritems(self.data): @@ -766,7 +765,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): frappe.db.sql(""" update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty} - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s @@ -794,7 +793,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): frappe.get_desk_link('Warehouse', args.warehouse), sle[0]["posting_date"], sle[0]["posting_time"], frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) - + frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): @@ -803,7 +802,7 @@ def get_future_sle_with_negative_qty(args): qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no from `tabStock Ledger Entry` - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index ea6b00fc58a..5d8ee5cab6d 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -40,7 +40,7 @@
{% if card.image %} -
+
{% endif %}
{{ card.title }}
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html index 3033d1587d6..876eaea8e6a 100644 --- a/erpnext/templates/pages/cart.html +++ b/erpnext/templates/pages/cart.html @@ -47,6 +47,9 @@ {% if doc.items %}
+ + {{ _("Continue Shopping") }} + {% if cart_settings.enable_checkout %}