Merge branch 'develop' into version-13-beta-pre-release
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 113bea0..533eda3 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -254,7 +254,8 @@
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 41f9ce0..a3c29b6 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 a749f0e..b0a864f 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -159,8 +159,8 @@
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 @@
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 4a952a3..06aa20b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -275,8 +275,11 @@
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 c64ffd8..451c936 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",
@@ -544,17 +545,6 @@
"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",
"oldfieldtype": "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 b52678e..dacd50a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -443,7 +443,7 @@
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 @@
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 @@
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 @@
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 @@
)
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 @@
"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 @@
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 @@
# 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 @@
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 f6d76e5..1f7853d 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 72199a9..f2a62cd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -130,16 +130,15 @@
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 6799fb9..447cee4 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 bbc86c4..f45e076 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.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.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 @@
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 @@
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 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 @@
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 @@
"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 @@
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 ee532da..7cd1828 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.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 @@
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 3695075..7a98aff 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 b46de6c..429a9f3 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 @@
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 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 9827e00..8eef2ad 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 @@
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
- <td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
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 eeb5140..cb4d9b4 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 @@
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 @@
`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 f54ceb0..998003a 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 @@
'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 @@
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 9399e70..8ac749d 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -14,13 +14,15 @@
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 @@
# 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 @@
"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 @@
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 @@
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 b6e61b1..cb2c98b 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -15,13 +15,14 @@
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 @@
# 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 @@
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 @@
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 @@
"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 @@
}
]
- 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 @@
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 @@
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 0b98274..dd0f065 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -164,16 +164,16 @@
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 @@
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 @@
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 75da71c..ee2beea 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 c7efb8a..d32e98e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -123,8 +123,8 @@
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 e537771..b76c378 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 @@
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
- per_ordered: ["<", 99.99],
+ per_ordered: ["<", 100],
company: me.frm.doc.company
}
})
@@ -280,7 +280,7 @@
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 0ee9d18..edeb135 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -52,7 +52,10 @@
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 a3b2085..a0187b0 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
@@ -44,7 +44,7 @@
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 0f1aa23..9d9d1b3 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -75,6 +75,9 @@
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 @@
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 @@
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 @@
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 @@
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 4dee375..ab1f027 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -44,7 +44,6 @@
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 @@
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 @@
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 @@
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 @@
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 6f9e1ef..e085048 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 @@
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 @@
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 @@
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 4399976..4b5e347 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -6,6 +6,7 @@
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 @@
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 @@
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 @@
""", (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 @@
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 += "<br><br>"
+ 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 8dd2e5b..76309f8 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -10,6 +10,7 @@
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 @@
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 14377e1..1c20555 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -416,9 +416,6 @@
'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 2c385e8..ab65260 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -88,7 +88,7 @@
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 @@
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 17a986d..bb692e1 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 @@
"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 f280402..ab586bc 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 @@
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 ff88052..adc8013 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 @@
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 da52ae9..f3660b3 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -747,3 +747,4 @@
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 0000000..79b6753
--- /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 d20c98c..d1ed91f 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -53,8 +53,7 @@
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 182ce0f..b2809b1 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
@@ -10,15 +10,7 @@
}
};
});
-
- 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 @@
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 6c7b382..1378bf0 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -58,13 +58,11 @@
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 e718031..1712081 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -207,22 +207,3 @@
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 2f15cbc..f0212db 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 db85a3e..a2a723d 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -195,6 +195,10 @@
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 @@
});
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 f144c29..9627600 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -105,10 +105,18 @@
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 @@
}
};
+ this.frm.trigger('set_default_internal_warehouse');
+
return frappe.run_serially([
() => set_value('currency', currency),
() => set_value('price_list_currency', currency),
@@ -589,12 +599,22 @@
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];
$.each(r.message, function(k, v) {
@@ -648,7 +668,7 @@
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 @@
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 @@
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 @@
}
},
+ 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 @@
"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 @@
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 @@
}, 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 770704e..808dd5a 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -276,6 +276,12 @@
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 713e8e3..f60de4c 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 7f76493..0000000
--- 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 8bd378c..0000000
--- 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 87baece..f09d3d0 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -40,14 +40,12 @@
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 46693ac..d6200c9 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -171,7 +171,7 @@
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 1e39c57..cbc9478 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -96,6 +96,8 @@
"""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 @@
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 @@
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 @@
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 9529923..59b878e 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -126,7 +126,8 @@
"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 @@
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 ad3de5f..96dc3f7 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -255,15 +255,16 @@
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 29214ee..bf8b7fc 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -84,7 +84,10 @@
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 d4fb07c..78f9df9 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -171,8 +171,10 @@
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 @@
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 3d64ac3..0a5c665 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 7f00fca..ce08464 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -399,6 +399,10 @@
}
},
+ 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 21ecaa1..9f50aba 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -178,6 +178,13 @@
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 9bd03d4..d3c442d 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -24,6 +24,16 @@
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 @@
// more
this.content.find('.btn-more').on('click', function() {
- me.start += 20;
+ me.start += me.page_length;
me.refresh();
});
@@ -69,33 +79,43 @@
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 @@
// 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");
- $(`<span class='text-muted small'> ${message} </span>`).appendTo(this.result);
+ var message = __("No Stock Available Currently");
+ this.content.find('.result').css('text-align', 'center');
+
+ $(`<div class='text-muted' style='margin: 20px 5px; font-weight: lighter;'>
+ ${message} </div>`).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 @@
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 @@
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 0000000..ab573e5
--- /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 9068e33..74cc42d 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 e41f1a8..97f85ba 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,6 +8,8 @@
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 @@
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 @@
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 ee18042..334bdea 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -95,13 +95,19 @@
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 @@
}
})
},
-
- 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 c9f8d08..f595aad 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",
@@ -503,18 +504,6 @@
"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",
"oldfieldtype": "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 a30cadf..fa5a7fb 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -664,7 +664,8 @@
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 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 @@
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 @@
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 4bbf3de..9de088d 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 faeeb57..ec32b0f 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -384,7 +384,10 @@
<a href="#stock-balance">' + __("Stock Levels") + '</a></h5>');
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 d07b3dc..fcf7c26 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 2729f4b..e4db048 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -15,5 +15,13 @@
frm.set_df_property("bulk_import_help", "options",
'<a href="#data-import-tool/Item Price">' + __("Import in Bulk") + '</a>');
+
+ 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 5f62381..83177b3 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 bed5ea9..e82a19b 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -54,7 +54,8 @@
"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 @@
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 b24d621..c77b993 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 64331c7..4fcdb4c 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 5de1352..1abbc35 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 @@
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 =
`<br><br>
<table class="table table-bordered" style="background-color: #f9f9f9;">
@@ -72,6 +62,11 @@
</table>`;
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 @@
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 @@
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 0149280..059f925 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 9ec6b89..69a8bf1 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.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 @@
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 @@
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 @@
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} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> 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 b97213e..144101c 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 @@
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 @@
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 @@
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 0d70958..de7a3d0 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 @@
} 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 d998729..57cc350 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -216,6 +216,10 @@
});
},
+ 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 5bb3095..32d349f 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
},
{
@@ -1107,6 +1112,12 @@
"read_only": 1
},
{
+ "default": "0",
+ "fieldname": "apply_putaway_rule",
+ "fieldtype": "Check",
+ "label": "Apply Putaway Rule"
+ },
+ {
"depends_on": "eval:!doc.__islocal",
"fieldname": "per_returned",
"fieldtype": "Percent",
@@ -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 f833fc7..550c849 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -83,6 +83,12 @@
}
])
+ 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 @@
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 @@
# 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 @@
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 @@
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 @@
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 e7a58b7..37e0c7f 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 @@
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 @@
"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 871b255..e991192 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"
},
{
@@ -840,6 +843,15 @@
"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 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/__init__.py
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 0000000..e056920
--- /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 0000000..a003f49
--- /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 0000000..ea26cac
--- /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:") + "<br><br>"
+ formatted_item_rows = ""
+
+ for entry in items_not_accomodated:
+ item_link = frappe.utils.get_link_to_form("Item", entry[0])
+ formatted_item_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(item_link, frappe.bold(entry[1]))
+
+ msg += """
+ <table class="table">
+ <thead>
+ <td>{0}</td>
+ <td>{1}</td>
+ </thead>
+ {2}
+ </table>
+ """.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 0000000..725e91e
--- /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 0000000..86f7dc3
--- /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 2fc4f96..274bbc2 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 @@
}
});
- 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 @@
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 @@
}
});
}
+ },
+
+ 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 @@
};
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 @@
}
}
- 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 5aed081..98c047a 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 4782a9d..d623d5c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -19,6 +19,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
"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 @@
"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 b78c6be..b12a854 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 @@
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 @@
"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 b78ae6d..988ae92 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"
@@ -496,6 +498,16 @@
"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",
"label": "Quantity"
@@ -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 5b40292..f0a90f9 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -30,6 +30,7 @@
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 3ff396b..84af57b 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 bf45251..dfe8fea 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -674,6 +674,8 @@
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 @@
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 @@
"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 0000000..f3f6196
--- /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 da21c6b..bddffd4 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -65,6 +65,9 @@
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/__init__.py
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 0000000..90112c7
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
@@ -0,0 +1,40 @@
+{% for d in data %}
+ <div class="dashboard-list-item" style="padding: 7px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small" style="margin-top: 8px;">
+ <a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <a data-type="item" data-name="{{ d.item_code }}">{{ d.item_code }}</a>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px; ">
+ {{ d.stock_capacity }}
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ {{ d.actual_qty }}
+ </div>
+ <div class="col-sm-2 small">
+ <div class="progress" title="Occupied Qty: {{ d.actual_qty }}" style="margin-bottom: 4px; height: 7px; margin-top: 14px;">
+ <div class="progress-bar" role="progressbar"
+ aria-valuenow="{{ d.percent_occupied }}"
+ aria-valuemin="0" aria-valuemax="100"
+ style="width:{{ d.percent_occupied }}%;
+ background-color: {{ d.color }}">
+ </div>
+ </div>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px;">
+ {{ d.percent_occupied }}%
+ </div>
+ {% if can_write %}
+ <div class="col-sm-1 text-right" style="margin-top: 2px;">
+ <button class="btn btn-default btn-xs btn-edit"
+ style="margin-top: 4px;margin-bottom: 4px;"
+ data-warehouse="{{ d.warehouse }}"
+ data-item="{{ escape(d.item_code) }}"
+ data-company="{{ escape(d.company) }}">{{ __("Edit Capacity") }}</a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+{% 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 0000000..b610e7d
--- /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 0000000..a6e5b45
--- /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 0000000..acaf180
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
@@ -0,0 +1,19 @@
+<div class="dashboard-list-item" style="padding: 12px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Warehouse
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Item
+ </div>
+ <div class="col-sm-1 small text-muted" style="margin-top: 8px;">
+ Stock Capacity
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Balance Stock Qty
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ % Occupied
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 5b9ada0..2b2a7a2 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -41,7 +41,7 @@
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 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 @@
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 @@
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 @@
# 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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 ea6b00f..5d8ee5c 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -40,7 +40,7 @@
<div class="col-md-{{ section.column_value }} mb-4">
<div class="card h-100 justify-content-between">
{% if card.image %}
- <div class="website-image-lazy" data-class="card-img-top h-100" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
+ <div class="website-image-lazy" data-class="card-img-top h-75" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ card.title }}</h5>
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index 3033d15..876eaea 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -47,6 +47,9 @@
{% if doc.items %}
<div class="place-order-container">
+ <a class="btn btn-primary-light mr-2" href="/all-products">
+ {{ _("Continue Shopping") }}
+ </a>
{% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order" type="button">
{{ _("Place Order") }}