Merge branch 'develop' into version-13-beta-pre-release

This commit is contained in:
Nabin Hait 2021-01-28 17:36:08 +05:30
commit a6213ccd61
106 changed files with 2923 additions and 630 deletions

View File

@ -254,7 +254,8 @@ def create_account(**kwargs):
account_name = kwargs.get('account_name'), account_name = kwargs.get('account_name'),
account_type = kwargs.get('account_type'), account_type = kwargs.get('account_type'),
parent_account = kwargs.get('parent_account'), parent_account = kwargs.get('parent_account'),
company = kwargs.get('company') company = kwargs.get('company'),
account_currency = kwargs.get('account_currency')
)) ))
account.save() account.save()

View File

@ -21,6 +21,7 @@
"book_asset_depreciation_entry_automatically", "book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template", "add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry", "automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
@ -219,6 +220,12 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Book Deferred Entries Based On", "label": "Book Deferred Entries Based On",
"options": "Days\nMonths" "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", "icon": "icon-cog",
@ -226,7 +233,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-10-13 11:32:52.268826", "modified": "2021-01-05 13:04:00.118892",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -159,8 +159,8 @@ class GLEntry(Document):
if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
and self.cost_center and _check_is_group(): 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 frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)
@ -170,7 +170,7 @@ class GLEntry(Document):
account_currency = get_account_currency(self.account) account_currency = get_account_currency(self.account)
if not self.account_currency: if not self.account_currency:
self.account_currency = company_currency self.account_currency = account_currency or company_currency
if account_currency != self.account_currency: if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")

View File

@ -275,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
supplier: function() { supplier: function() {
var me = this; 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; return;
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{ {
posting_date: this.frm.doc.posting_date, posting_date: this.frm.doc.posting_date,

View File

@ -57,8 +57,8 @@
"set_warehouse", "set_warehouse",
"rejected_warehouse", "rejected_warehouse",
"col_break_warehouse", "col_break_warehouse",
"set_from_warehouse",
"is_subcontracted", "is_subcontracted",
"supplier_warehouse",
"items_section", "items_section",
"update_stock", "update_stock",
"scan_barcode", "scan_barcode",
@ -515,6 +515,7 @@
}, },
{ {
"depends_on": "update_stock", "depends_on": "update_stock",
"description": "Sets 'Accepted Warehouse' in each row of the items table.",
"fieldname": "set_warehouse", "fieldname": "set_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Set Accepted Warehouse", "label": "Set Accepted Warehouse",
@ -543,17 +544,6 @@
"options": "No\nYes", "options": "No\nYes",
"print_hide": 1 "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", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -1232,7 +1222,9 @@
"fieldname": "inter_company_invoice_reference", "fieldname": "inter_company_invoice_reference",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Inter Company Invoice Reference", "label": "Inter Company Invoice Reference",
"no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@ -1356,13 +1348,25 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Represents Company", "label": "Represents Company",
"options": "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", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-11 12:46:12.796378", "modified": "2020-12-26 20:49:03.305063",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController):
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total 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(): 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, grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total")) self.precision("grand_total"))
gl_entries.append( gl_entries.append(
@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {} voucher_wise_stock_value = {}
if self.update_stock: if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry', for d in frappe.get_all('Stock Ledger Entry',
fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) 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") valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation') if d.category in ('Valuation', 'Total and Valuation')
@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController):
) )
else: else:
gl_entries.append( if not self.is_internal_transfer():
self.get_gl_dict({ gl_entries.append(
"account": item.expense_account, self.get_gl_dict({
"against": self.supplier, "account": item.expense_account,
"debit": warehouse_debit_amount, "against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": warehouse_debit_amount,
"cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"project": item.project or self.project "cost_center": item.cost_center,
}, account_currency, item=item) "project": item.project or self.project
) }, account_currency, item=item)
)
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if landed_cost_entries: if landed_cost_entries:
@ -582,7 +583,8 @@ class PurchaseInvoice(BuyingController):
"against": item.expense_account, "against": item.expense_account,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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 "project": item.project or self.project
}, item=item)) }, item=item))
@ -624,13 +626,14 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr: if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account expense_account = service_received_but_not_billed_account
gl_entries.append(self.get_gl_dict({ if not self.is_internal_transfer():
"account": expense_account, gl_entries.append(self.get_gl_dict({
"against": self.supplier, "account": expense_account,
"debit": amount, "against": self.supplier,
"cost_center": item.cost_center, "debit": amount,
"project": item.project or self.project "cost_center": item.cost_center,
}, account_currency, item=item)) "project": item.project or self.project
}, account_currency, item=item))
# If asset is bought through this document and not linked to PR # If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount: if self.update_stock and item.landed_cost_voucher_amount:
@ -795,10 +798,10 @@ class PurchaseInvoice(BuyingController):
# Stock ledger value is not matching with the warehouse amount # Stock ledger value is not matching with the warehouse amount
if (self.update_stock and voucher_wise_stock_value.get(item.name) and 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") 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 stock_adjustment_amt = warehouse_debit_amount - stock_amount
gl_entries.append( gl_entries.append(

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -87,6 +88,7 @@
"po_detail", "po_detail",
"purchase_receipt", "purchase_receipt",
"pr_detail", "pr_detail",
"sales_invoice_item",
"item_weight_details", "item_weight_details",
"weight_per_unit", "weight_per_unit",
"total_weight", "total_weight",
@ -553,8 +555,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"label": "Brand", "label": "Brand",
"print_hide": 1, "options": "Brand",
"options": "Brand" "print_hide": 1
}, },
{ {
"fetch_from": "item_code.item_group", "fetch_from": "item_code.item_group",
@ -562,9 +564,9 @@
"fieldname": "item_group", "fieldname": "item_group",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Item Group", "label": "Item Group",
"options": "Item Group",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"options": "Item Group"
}, },
{ {
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", "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 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
"fieldname": "from_warehouse", "fieldname": "from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Supplier Warehouse", "label": "From Warehouse",
"options": "Warehouse" "options": "Warehouse"
}, },
{ {
@ -779,11 +782,20 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 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, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2020-08-20 11:48:01.398356", "links": [],
"modified": "2020-12-26 17:20:36.415791",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -130,16 +130,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.set_default_print_format(); this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
frappe.model.with_doc("Customer", me.frm.doc.customer, function() { let internal = me.frm.doc.is_internal_customer;
var customer = frappe.model.get_doc("Customer", me.frm.doc.customer); if (internal) {
var internal = customer.is_internal_customer; let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" :
var disabled = customer.disabled; "Inter Company Purchase Invoice";
if (internal == 1 && disabled == 0) {
me.frm.add_custom_button("Inter Company Invoice", function() { me.frm.add_custom_button(button_label, function() {
me.make_inter_company_invoice(); me.make_inter_company_invoice();
}, __('Create')); }, __('Create'));
} }
});
} }
}, },

View File

@ -60,6 +60,8 @@
"ignore_pricing_rule", "ignore_pricing_rule",
"sec_warehouse", "sec_warehouse",
"set_warehouse", "set_warehouse",
"column_break_55",
"set_target_warehouse",
"items_section", "items_section",
"update_stock", "update_stock",
"scan_barcode", "scan_barcode",
@ -1969,13 +1971,24 @@
"label": "Represents Company", "label": "Represents Company",
"options": "Company", "options": "Company",
"read_only": 1 "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", "icon": "fa fa-file-text",
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-11 12:48:31.769958", "modified": "2020-12-25 22:57:32.555067",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -6,7 +6,7 @@ import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw 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 frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date 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 from erpnext.healthcare.utils import manage_invoice_submit_cancel
@ -1537,7 +1539,7 @@ def validate_inter_company_transaction(doc, doctype):
details = get_inter_company_details(doc, doctype) 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 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}) 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.")) frappe.throw(_("Selected Price List should have buying and selling fields checked."))
party = details.get("party") party = details.get("party")
@ -1560,6 +1562,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]: if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name) source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" 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' source_document_warehouse_field = 'target_warehouse'
target_document_warehouse_field = 'from_warehouse' target_document_warehouse_field = 'from_warehouse'
else: else:
@ -1573,6 +1576,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.run_method("set_missing_values") target.run_method("set_missing_values")
set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent): def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name target_doc.inter_company_invoice_reference = source_doc.name
@ -1580,19 +1584,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency')
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.supplier = details.get("party") 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 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: if currency:
target_doc.currency = 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: else:
currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency')
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.customer = details.get("party") target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list 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: if currency:
target_doc.currency = 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 = { item_field_map = {
"doctype": target_doctype + " Item", "doctype": target_doctype + " Item",
"field_no_map": [ "field_no_map": [
@ -1600,25 +1623,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"expense_account", "expense_account",
"cost_center", "cost_center",
"warehouse" "warehouse"
] ],
"field_map": {
'rate': 'rate',
}
} }
if source_doc.get('update_stock'): if doctype in ["Sales Invoice", "Sales Order"]:
item_field_map.update({ item_field_map["field_map"].update({
'field_map': { "name": target_detail_field,
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
}
}) })
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, { doclist = get_mapped_doc(doctype, source_name, {
doctype: { doctype: {
"doctype": target_doctype, "doctype": target_doctype,
"postprocess": update_details, "postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": [ "field_no_map": [
"taxes_and_charges" "taxes_and_charges",
"set_warehouse",
"shipping_address"
] ]
}, },
doctype +" Item": item_field_map doctype +" Item": item_field_map
@ -1627,6 +1658,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
return doclist 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() @frappe.whitelist()
def get_loyalty_programs(customer): def get_loyalty_programs(customer):
''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' ''' sets applicable loyalty program to the customer or returns a list of applicable programs '''

View File

@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.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.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.utils import get_incoming_rate
class TestSalesInvoice(unittest.TestCase): class TestSalesInvoice(unittest.TestCase):
def make(self): def make(self):
@ -1770,59 +1771,82 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier") self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
# def test_internal_transfer_gl_entry(self): def test_internal_transfer_gl_entry(self):
# ## Create internal transfer account ## Create internal transfer account
# account = create_account(account_name="Unrealized Profit", account = create_account(account_name="Unrealized Profit",
# parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
# frappe.db.set_value('Company', '_Test Company with perpetual inventory', frappe.db.set_value('Company', '_Test Company with perpetual inventory',
# 'unrealized_profit_loss_account', account) 'unrealized_profit_loss_account', account)
# customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory") "_Test Company with perpetual inventory")
# create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory") "_Test Company with perpetual inventory")
# si = create_sales_invoice( si = create_sales_invoice(
# company = "_Test Company with perpetual inventory", company = "_Test Company with perpetual inventory",
# customer = customer, customer = customer,
# debit_to = "Debtors - TCP1", debit_to = "Debtors - TCP1",
# warehouse = "Stores - TCP1", warehouse = "Stores - TCP1",
# income_account = "Sales - TCP1", income_account = "Sales - TCP1",
# expense_account = "Cost of Goods Sold - TCP1", expense_account = "Cost of Goods Sold - TCP1",
# cost_center = "Main - TCP1", cost_center = "Main - TCP1",
# currency = "INR", currency = "INR",
# do_not_save = 1 do_not_save = 1
# ) )
# si.selling_price_list = "_Test Price List Rest of the World" si.selling_price_list = "_Test Price List Rest of the World"
# si.update_stock = 1 si.update_stock = 1
# si.items[0].target_warehouse = 'Work In Progress - TCP1' si.items[0].target_warehouse = 'Work In Progress - TCP1'
# add_taxes(si) add_taxes(si)
# si.save() si.save()
# si.submit()
# target_doc = make_inter_company_transaction("Sales Invoice", si.name) rate = 0.0
# target_doc.company = '_Test Company with perpetual inventory' for d in si.get('items'):
# target_doc.items[0].warehouse = 'Finished Goods - TCP1' rate = get_incoming_rate({
# add_taxes(target_doc) "item_code": d.item_code,
# target_doc.save() "warehouse": d.warehouse,
# target_doc.submit() "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 = [ rate = flt(rate, 2)
# ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
# ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
# ]
# check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) si.submit()
# pi_gl_entries = [ target_doc = make_inter_company_transaction("Sales Invoice", si.name)
# ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], target_doc.company = '_Test Company with perpetual inventory'
# ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] 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): def test_eway_bill_json(self):
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()

View File

@ -565,11 +565,12 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
"fieldname": "target_warehouse", "fieldname": "target_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Customer Warehouse (Optional)", "label": "Target Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1 "print_hide": 1
@ -815,7 +816,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-23 19:59:04.879322", "modified": "2020-12-26 17:25:04.090630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc):
validate_disabled(doc) validate_disabled(doc)
# Validate with existing taxes and charges template for unique tax category
validate_for_tax_category(doc)
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)
@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc):
def validate_disabled(doc): def validate_disabled(doc):
if doc.is_default and doc.disabled: if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template")) 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)))

View File

@ -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(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(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(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.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr> </tr>

View File

@ -49,7 +49,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
elif d.po_detail: elif d.po_detail:
purchase_receipt = ", ".join(po_pr_map.get(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 = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
@ -315,6 +316,7 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `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_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,

View File

@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'company': d.company, 'company': d.company,
'sales_order': d.sales_order, 'sales_order': d.sales_order,
'delivery_note': d.delivery_note, '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, 'cost_center': d.cost_center,
'stock_qty': d.stock_qty, 'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom 'stock_uom': d.stock_uom
@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns):
select select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `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`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,

View File

@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if not filters: filters = {} if not filters: filters = {}
invoice_list = get_invoices(filters, additional_query_columns) 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: if not invoice_list:
msgprint(_("No record found")) msgprint(_("No record found"))
return columns, invoice_list return columns, invoice_list
invoice_expense_map = get_invoice_expense_map(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, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts) invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
# map expense values # map expense values
base_net_total = 0 base_net_total = 0
for expense_acc in expense_accounts: 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 base_net_total += expense_amount
row.append(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 # net total
row.append(base_net_total or inv.base_net_total) row.append(base_net_total or inv.base_net_total)
@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns):
"width": 80 "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: if invoice_list:
expense_accounts = frappe.db.sql_list("""select distinct expense_account expense_accounts = frappe.db.sql_list("""select distinct expense_account
@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" % and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) ', '.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] 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: for account in tax_accounts:
if account not in expense_accounts: if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120") 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", [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120",
_("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":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): def get_conditions(filters):
conditions = "" conditions = ""
@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list):
return invoice_expense_map 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): def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
tax_details = frappe.db.sql(""" tax_details = frappe.db.sql("""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)

View File

@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
if not filters: filters = frappe._dict({}) if not filters: filters = frappe._dict({})
invoice_list = get_invoices(filters, additional_query_columns) 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: if not invoice_list:
msgprint(_("No record found")) msgprint(_("No record found"))
return columns, invoice_list return columns, invoice_list
invoice_income_map = get_invoice_income_map(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, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_income_map, income_accounts) invoice_income_map, income_accounts)
#Cost Center & Warehouse Map #Cost Center & Warehouse Map
@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
# map income values # map income values
base_net_total = 0 base_net_total = 0
for income_acc in income_accounts: 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 base_net_total += income_amount
row.update({ row.update({
frappe.scrub(income_acc): income_amount 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 # net total
row.update({'net_total': base_net_total or inv.base_net_total}) row.update({'net_total': base_net_total or inv.base_net_total})
@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns):
tax_accounts = [] tax_accounts = []
income_columns = [] income_columns = []
tax_columns = [] tax_columns = []
unrealized_profit_loss_accounts = []
unrealized_profit_loss_account_columns = []
if invoice_list: if invoice_list:
income_accounts = frappe.db.sql_list("""select distinct income_account income_accounts = frappe.db.sql_list("""select distinct income_account
@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" % and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) ', '.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: for account in income_accounts:
income_columns.append({ income_columns.append({
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "width": 120
}) })
@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns):
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "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 = [{ net_total_column = [{
"label": _("Net Total"), "label": _("Net Total"),
"fieldname": "net_total", "fieldname": "net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "width": 120
}] }]
@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns):
} }
] ]
columns = columns + income_columns + net_total_column + tax_columns + total_columns columns = columns + income_columns + unrealized_profit_loss_account_columns + \
net_total_column + tax_columns + total_columns
return columns, income_accounts, tax_accounts return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns):
return frappe.db.sql(""" return frappe.db.sql("""
select name, posting_date, debit_to, project, customer, select name, posting_date, debit_to, project, customer,
customer_name, owner, remarks, territory, tax_id, customer_group, 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` from `tabSales Invoice`
where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') %
conditions, filters, as_dict=1) conditions, filters, as_dict=1)
@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list):
return invoice_income_map 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): def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
tax_details = frappe.db.sql("""select parent, account_head, tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount sum(base_tax_amount_after_discount_amount) as tax_amount

View File

@ -164,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if (doc.docstatus === 1 && !doc.inter_company_order_reference) { if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this; let me = this;
frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => { let internal = me.frm.doc.is_internal_supplier;
let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier); if (internal) {
let internal = supplier.is_internal_supplier; let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" :
let disabled = supplier.disabled; "Inter Company Sales Order";
if (internal === 1 && disabled === 0) {
me.frm.add_custom_button("Inter Company Order", function() { me.frm.add_custom_button(button_label, function() {
me.make_inter_company_order(me.frm); me.make_inter_company_order(me.frm);
}, __('Create')); }, __('Create'));
} }
});
} }
} }
@ -353,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
make_purchase_receipt: function() { make_purchase_receipt: function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
frm: cur_frm frm: cur_frm,
freeze_message: __("Creating Purchase Receipt ...")
}) })
}, },
@ -380,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })

View File

@ -134,6 +134,8 @@
"ref_sq", "ref_sq",
"column_break_74", "column_break_74",
"party_account_currency", "party_account_currency",
"is_internal_supplier",
"represents_company",
"inter_company_order_reference" "inter_company_order_reference"
], ],
"fields": [ "fields": [
@ -1101,13 +1103,28 @@
{ {
"fieldname": "items_col_break", "fieldname": "items_col_break",
"fieldtype": "Column 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", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-03 16:46:44.229351", "modified": "2021-01-20 22:07:23.487138",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController):
if self.is_subcontracted == "Yes": if self.is_subcontracted == "Yes":
for item in self.items: for item in self.items:
if not item.bom: if not item.bom:
frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}")
.format(item.item_code, item.idx))) .format(item.item_code, item.idx))
def get_schedule_dates(self): def get_schedule_dates(self):
for d in self.get('items'): for d in self.get('items'):

View File

@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })
@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99] per_ordered: ["<", 100]
} }
}); });
dialog.hide(); dialog.hide();

View File

@ -52,7 +52,10 @@ class Supplier(TransactionBase):
self.validate_internal_supplier() self.validate_internal_supplier()
def validate_internal_supplier(self): 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.throw(_("Internal Supplier for company {0} already exists").format(
frappe.bold(self.represents_company))) frappe.bold(self.represents_company)))

View File

@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })

View File

@ -75,6 +75,9 @@ class AccountsController(TransactionBase):
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year() self.validate_date_with_fiscal_year()
self.validate_inter_company_reference()
self.set_incoming_rate()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
@ -119,6 +122,12 @@ class AccountsController(TransactionBase):
def before_cancel(self): def before_cancel(self):
validate_einvoice_fields(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): def validate_deferred_start_and_end_date(self):
for d in self.items: for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@ -206,6 +215,17 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self) 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): def validate_due_date(self):
if self.get('is_pos'): return if self.get('is_pos'): return
@ -448,8 +468,10 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(gl_dict.account) account_currency = get_account_currency(gl_dict.account)
if gl_dict.account and self.doctype not in ["Journal Entry", 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) 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"), set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"),
self.company_currency) self.company_currency)
@ -962,9 +984,9 @@ class AccountsController(TransactionBase):
It will an internal transfer if its an internal customer and representation It will an internal transfer if its an internal customer and representation
company is same as billing company 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' internal_party_field = 'is_internal_customer'
else: elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
internal_party_field = 'is_internal_supplier' internal_party_field = 'is_internal_supplier'
if self.get(internal_party_field) and (self.represents_company == self.company): if self.get(internal_party_field) and (self.represents_company == self.company):

View File

@ -44,7 +44,6 @@ class BuyingController(StockController):
self.validate_items() self.validate_items()
self.set_qty_as_per_stock_uom() self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items() self.validate_stock_or_nonstock_items()
self.update_tax_category_for_internal_transfer()
self.validate_warehouse() self.validate_warehouse()
self.validate_from_warehouse() self.validate_from_warehouse()
self.set_supplier_address() self.set_supplier_address()
@ -100,11 +99,6 @@ class BuyingController(StockController):
msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
self.update_tax_category(msg) 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): def update_tax_category(self, msg):
tax_for_valuation = [d for d in self.get("taxes") tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]] if d.category in ["Valuation", "Valuation and Total"]]
@ -224,6 +218,48 @@ class BuyingController(StockController):
else: else:
item.valuation_rate = 0.0 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): def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
@ -559,6 +595,8 @@ class BuyingController(StockController):
from_warehouse_sle = self.get_sl_entries(d, { from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty, "actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse, "warehouse": d.from_warehouse,
"outgoing_rate": d.rate,
"recalculate_rate": 1,
"dependant_sle_voucher_detail_no": d.name "dependant_sle_voucher_detail_no": d.name
}) })

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe 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 frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@ -49,7 +49,6 @@ class SellingController(StockController):
self.set_customer_address() self.set_customer_address()
self.validate_for_duplicate_items() self.validate_for_duplicate_items()
self.validate_target_warehouse() self.validate_target_warehouse()
self.set_incoming_rate()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@ -312,7 +311,7 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows) sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self): 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 return
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
@ -322,15 +321,26 @@ class SellingController(StockController):
d.incoming_rate = get_incoming_rate({ d.incoming_rate = get_incoming_rate({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "warehouse": d.warehouse,
"posting_date": self.posting_date, "posting_date": self.get('posting_date') or self.get('transaction_date'),
"posting_time": self.posting_time, "posting_time": self.get('posting_time') or nowtime(),
"qty": -1*flt(d.qty), "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')),
"serial_no": d.serial_no, "serial_no": d.get('serial_no'),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation") "allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False) }, 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"): elif self.get("return_against"):
# Get incoming rate of return entry from reference document # Get incoming rate of return entry from reference document
# based on original item cost as per valuation method # based on original item cost as per valuation method

View File

@ -6,6 +6,7 @@ import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _ from frappe import _
import frappe.defaults 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.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.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -23,6 +24,8 @@ class StockController(AccountsController):
self.validate_inspection() self.validate_inspection()
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.validate_internal_transfer()
self.validate_putaway_capacity()
def make_gl_entries(self, gl_entries=None, from_repost=False): def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2: if self.docstatus == 2:
@ -72,6 +75,7 @@ class StockController(AccountsController):
warehouse_with_no_account = [] warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency") precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details: for item_row in voucher_details:
sle_list = sle_map.get(item_row.name) sle_list = sle_map.get(item_row.name)
if sle_list: if sle_list:
for sle in sle_list: for sle in sle_list:
@ -216,7 +220,7 @@ class StockController(AccountsController):
""", (self.doctype, self.name), as_dict=True) """, (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries: 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 return stock_ledger
def make_batches(self, warehouse_field): def make_batches(self, warehouse_field):
@ -391,6 +395,84 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 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): def repost_future_sle_and_gle(self):
args = frappe._dict({ args = frappe._dict({
"posting_date": self.posting_date, "posting_date": self.posting_date,

View File

@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template 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.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): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
@ -758,3 +759,35 @@ def get_rounded_tax_amount(itemised_tax, precision):
for taxes in itemised_tax.values(): for taxes in itemised_tax.values():
for tax_account in taxes: for tax_account in taxes:
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) 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"))

View File

@ -416,9 +416,6 @@ regional_overrides = {
'Italy': { 'Italy': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', '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', '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 = [ user_privacy_documents = [

View File

@ -88,7 +88,7 @@ def get_events(start, end, filters=None):
def add_assignments(events, start, end, conditions=None): def add_assignments(events, start, end, conditions=None):
query = """select name, start_date, end_date, employee_name, query = """select name, start_date, end_date, employee_name,
employee, docstatus employee, docstatus, shift_type
from `tabShift Assignment` where from `tabShift Assignment` where
start_date >= %(start_date)s start_date >= %(start_date)s
or end_date <= %(end_date)s or end_date <= %(end_date)s
@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None):
if conditions: if conditions:
query += conditions query += conditions
for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True)
e = { shift_timing_map = get_shift_type_timing([d.shift_type for d in records])
"name": d.name,
"doctype": "Shift Assignment", for d in records:
"start_date": d.start_date, daily_event_start = d.start_date
"end_date": d.end_date if d.end_date else nowdate(), daily_event_end = d.end_date if d.end_date else getdate()
"title": cstr(d.employee_name) + ": "+ \ delta = timedelta(days=1)
cstr(d.shift_type), while daily_event_start <= daily_event_end:
"docstatus": d.docstatus 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']
if e not in events: daily_event_start += delta
events.append(e) 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): def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):

View File

@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = {
"start": "start_date", "start": "start_date",
"end": "end_date", "end": "end_date",
"id": "name", "id": "name",
"docstatus": 1 "docstatus": 1,
}, "allDay": "allDay",
options: {
header: {
left: 'prev,next today',
center: 'title',
right: 'month'
}
}, },
get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events" get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events"
} }

View File

@ -43,22 +43,24 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company')) currency = erpnext.get_company_currency(filters.get('company'))
for key, qty in iteritems(pledge_values): for key, qty in iteritems(pledge_values):
row = {} if qty:
current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) row = {}
valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') 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(loan_security_details.get(key[1]))
row.update({ row.update({
'applicant_type': applicant_type_map.get(key[0]), 'applicant_type': applicant_type_map.get(key[0]),
'applicant_name': key[0], 'applicant_name': key[0],
'total_qty': qty, 'total_qty': qty,
'current_value': current_value, 'current_value': current_value,
'price_valid_upto': valid_upto, 'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \
'currency': currency else 0.0,
}) 'currency': currency
})
data.append(row) data.append(row)
return data return data

View File

@ -40,21 +40,22 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company')) currency = erpnext.get_company_currency(filters.get('company'))
for security, value in iteritems(current_pledges): for security, value in iteritems(current_pledges):
row = {} if value.get('qty'):
current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) row = {}
valid_upto = loan_security_details.get(security, {}).get('valid_upto') 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(loan_security_details.get(security))
row.update({ row.update({
'total_qty': value.get('qty'), 'total_qty': value.get('qty'),
'current_value': current_value, 'current_value': current_value,
'price_valid_upto': valid_upto, 'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
'pledged_applicant_count': value.get('applicant_count'), 'pledged_applicant_count': value.get('applicant_count'),
'currency': currency 'currency': currency
}) })
data.append(row) data.append(row)
return data return data

View File

@ -747,3 +747,4 @@ erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field 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.setup_patient_history_settings_for_standard_doctypes
erpnext.patches.v13_0.add_naming_series_to_old_projects

View File

@ -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()

View File

@ -53,8 +53,7 @@ frappe.ui.form.on('Additional Salary', {
if (!frm.doc.company) return; if (!frm.doc.company) return;
frm.set_query("salary_component", function() { frm.set_query("salary_component", function() {
return { return {
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
filters: {type: "earning", company: frm.doc.company}
}; };
}); });
}, },

View File

@ -10,15 +10,7 @@ frappe.ui.form.on('Employee Incentive', {
} }
}; };
}); });
frm.trigger('set_earning_component');
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}
};
});
}, },
employee: function(frm) { employee: function(frm) {
@ -45,11 +37,21 @@ frappe.ui.form.on('Employee Incentive', {
callback: function(data) { callback: function(data) {
if (data.message) { if (data.message) {
frm.set_value("company", data.message.company); 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) { get_employee_currency: function(frm) {
frappe.call({ frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",

View File

@ -58,13 +58,11 @@ frappe.ui.form.on('Salary Structure', {
if(!frm.doc.company) return; if(!frm.doc.company) return;
frm.set_query("salary_component", "earnings", function() { frm.set_query("salary_component", "earnings", function() {
return { return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "earning", company: frm.doc.company} filters: {type: "earning", company: frm.doc.company}
}; };
}); });
frm.set_query("salary_component", "deductions", function() { frm.set_query("salary_component", "deductions", function() {
return { return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "deduction", company: frm.doc.company} filters: {type: "deduction", company: frm.doc.company}
}; };
}); });

View File

@ -207,22 +207,3 @@ def get_employees(salary_structure):
return list(set([d.employee for d in employees])) 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']
})

View File

@ -55,6 +55,8 @@
"js/item-dashboard.min.js": [ "js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.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"
] ]
} }

View File

@ -195,6 +195,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this._super(doc, cdt, cdn); this._super(doc, cdt, cdn);
}, },
batch_no: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn);
},
received_qty: function(doc, cdt, cdn) { received_qty: function(doc, cdt, cdn) {
this.calculate_accepted_qty(doc, cdt, cdn) this.calculate_accepted_qty(doc, cdt, cdn)
}, },

View File

@ -105,10 +105,18 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.ui.form.on(this.frm.doctype + " Item", { frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) { items_add: function(frm, cdt, cdn) {
var item = frappe.get_doc(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; 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'); erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
} }
}); });
@ -227,6 +235,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}; };
this.frm.trigger('set_default_internal_warehouse');
return frappe.run_serially([ return frappe.run_serially([
() => set_value('currency', currency), () => set_value('currency', currency),
() => set_value('price_list_currency', currency), () => set_value('price_list_currency', currency),
@ -589,11 +599,21 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => { .then((r) => {
if (r.message && 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; 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) { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
@ -648,7 +668,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
args: item_args args: item_args
}, },
callback: function(r) { callback: function(r) {
frappe.model.set_value(item.doctype, item.name, 'rate', r.message); frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor);
} }
}); });
}, },
@ -714,6 +734,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.calculate_taxes_and_totals(false); 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() { company: function() {
var me = this; var me = this;
var set_pricing = function() { var set_pricing = function() {
@ -800,7 +845,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){ erpnext.utils.get_shipping_address(this.frm, function(){
set_party_account(set_pricing); set_party_account(set_pricing);
}) });
// Get default company billing address in Purchase Invoice, Order and Receipt // Get default company billing address in Purchase Invoice, Order and Receipt
frappe.call({ frappe.call({
@ -1099,6 +1144,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}, },
batch_no: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
this.apply_price_list(item, true);
},
toggle_conversion_factor: function(item) { toggle_conversion_factor: function(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same // 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) { if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
@ -1403,6 +1453,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
"pricing_rules": d.pricing_rules, "pricing_rules": d.pricing_rules,
"warehouse": d.warehouse, "warehouse": d.warehouse,
"serial_no": d.serial_no, "serial_no": d.serial_no,
"batch_no": d.batch_no,
"price_list_rate": d.price_list_rate, "price_list_rate": d.price_list_rate,
"conversion_factor": d.conversion_factor || 1.0 "conversion_factor": d.conversion_factor || 1.0
}); });
@ -1961,6 +2012,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); 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) { autofill_warehouse : function (child_table, warehouse_field, warehouse) {
if (warehouse && child_table && child_table.length) { if (warehouse && child_table && child_table.length) {
let doctype = child_table[0].doctype; let doctype = child_table[0].doctype;
@ -2025,3 +2084,35 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
}, show_dialog); }, 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();
}
}
});
};

View File

@ -276,6 +276,12 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
erpnext.utils.get_shipping_address = function(frm, callback){ erpnext.utils.get_shipping_address = function(frm, callback){
if (frm.doc.company) { 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({ frappe.call({
method: "erpnext.accounts.custom.address.get_shipping_address", method: "erpnext.accounts.custom.address.get_shipping_address",
args: { args: {

View File

@ -7,13 +7,14 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"client", "client",
"account_number_length",
"column_break_2",
"client_number", "client_number",
"section_break_4", "column_break_2",
"consultant_number",
"consultant", "consultant",
"section_break_4",
"account_number_length",
"column_break_6", "column_break_6",
"consultant_number" "temporary_against_account_number"
], ],
"fields": [ "fields": [
{ {
@ -66,10 +67,17 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Account Number Length", "label": "Account Number Length",
"reqd": 1 "reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "temporary_against_account_number",
"fieldtype": "Data",
"label": "Temporary Against Account Number",
"reqd": 1
} }
], ],
"links": [], "links": [],
"modified": "2020-11-05 17:52:11.674329", "modified": "2020-11-19 19:00:09.088816",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "DATEV Settings", "name": "DATEV Settings",

View File

@ -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)

View File

@ -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)

View File

@ -40,14 +40,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
callback: function(r) { callback: function(r) {
if(r.message) { if(r.message) {
frm.set_value('taxes_and_charges', r.message.taxes_and_charges); 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); 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', []);
} }
} }
}); });
} }
}); });
}; }

View File

@ -171,7 +171,7 @@ def get_regional_address_details(party_details, doctype, company):
if is_internal_transfer(party_details, doctype): if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = '' party_details.taxes_and_charges = ''
party_details.taxes = '' party_details.taxes = []
return party_details return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):

View File

@ -96,6 +96,8 @@ def execute(filters=None):
"""Entry point for frappe.""" """Entry point for frappe."""
data = [] data = []
if filters and validate(filters): 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) data = get_transactions(filters, as_dict=0)
return COLUMNS, data return COLUMNS, data
@ -156,10 +158,10 @@ def get_transactions(filters, as_dict=1):
case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen',
/* account number or, if empty, party account number */ /* 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 */ /* 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.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1', gl.voucher_no as 'Belegfeld 1',
@ -171,27 +173,10 @@ def get_transactions(filters, as_dict=1):
FROM `tabGL Entry` gl FROM `tabGL Entry` gl
/* Statistisches Konto (Debitoren/Kreditoren) */
left join `tabParty Account` pa
on gl.against = pa.parent
and gl.company = pa.company
/* Kontonummer */ /* Kontonummer */
left join `tabAccount` acc left join `tabAccount` acc
on gl.account = acc.name 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 WHERE gl.company = %(company)s
AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) >= %(from_date)s
AND DATE(gl.posting_date) <= %(to_date)s AND DATE(gl.posting_date) <= %(to_date)s
@ -347,7 +332,9 @@ def download_datev_csv(filters):
coa = frappe.get_value('Company', company, 'chart_of_accounts') coa = frappe.get_value('Company', company, 'chart_of_accounts')
filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') 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) transactions = get_transactions(filters)
account_names = get_account_names(filters) account_names = get_account_names(filters)

View File

@ -126,7 +126,8 @@ def make_datev_settings(company):
"doctype": "DATEV Settings", "doctype": "DATEV Settings",
"client": company.name, "client": company.name,
"client_number": "12345", "client_number": "12345",
"consultant_number": "67890" "consultant_number": "67890",
"temporary_against_account_number": "9999"
}).insert() }).insert()
@ -137,7 +138,8 @@ class TestDatev(TestCase):
self.filters = { self.filters = {
"company": self.company.name, "company": self.company.name,
"from_date": today(), "from_date": today(),
"to_date": today() "to_date": today(),
"temporary_against_account_number": "9999"
} }
make_datev_settings(self.company) make_datev_settings(self.company)

View File

@ -255,15 +255,16 @@ class Gstr1Report(object):
for item_code, tax_amounts in item_wise_tax_detail.items(): for item_code, tax_amounts in item_wise_tax_detail.items():
tax_rate = tax_amounts[0] tax_rate = tax_amounts[0]
if cgst_or_sgst: if tax_rate:
tax_rate *= 2 if cgst_or_sgst:
if parent not in self.cgst_sgst_invoices: tax_rate *= 2
self.cgst_sgst_invoices.append(parent) if parent not in self.cgst_sgst_invoices:
self.cgst_sgst_invoices.append(parent)
rate_based_dict = self.items_based_on_tax_rate\ rate_based_dict = self.items_based_on_tax_rate\
.setdefault(parent, {}).setdefault(tax_rate, []) .setdefault(parent, {}).setdefault(tax_rate, [])
if item_code not in rate_based_dict: if item_code not in rate_based_dict:
rate_based_dict.append(item_code) rate_based_dict.append(item_code)
except ValueError: except ValueError:
continue continue
if unidentified_gst_accounts: if unidentified_gst_accounts:

View File

@ -84,7 +84,10 @@ class Customer(TransactionBase):
frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)))
def validate_internal_customer(self): 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.throw(_("Internal Customer for company {0} already exists").format(
frappe.bold(self.represents_company))) frappe.bold(self.represents_company)))

View File

@ -171,8 +171,10 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); 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')); this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create'));
}
// maintenance // maintenance
if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
@ -193,16 +195,15 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
if (doc.docstatus === 1 && !doc.inter_company_order_reference) { if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this; let me = this;
frappe.model.with_doc("Customer", me.frm.doc.customer, () => { let internal = me.frm.doc.is_internal_customer;
let customer = frappe.model.get_doc("Customer", me.frm.doc.customer); if (internal) {
let internal = customer.is_internal_customer; let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Order" :
let disabled = customer.disabled; "Inter Company Purchase Order";
if (internal === 1 && disabled === 0) {
me.frm.add_custom_button("Inter Company Order", function() { me.frm.add_custom_button(button_label, function() {
me.make_inter_company_order(); me.make_inter_company_order();
}, __('Create')); }, __('Create'));
} }
});
} }
} }
// payment request // payment request

View File

@ -107,6 +107,8 @@
"tc_name", "tc_name",
"terms", "terms",
"more_info", "more_info",
"is_internal_customer",
"represents_company",
"inter_company_order_reference", "inter_company_order_reference",
"project", "project",
"party_account_currency", "party_account_currency",
@ -1103,7 +1105,8 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Inter Company Order Reference", "label": "Inter Company Order Reference",
"options": "Purchase Order" "options": "Purchase Order",
"read_only": 1
}, },
{ {
"description": "Track this Sales Order against any Project", "description": "Track this Sales Order against any Project",
@ -1455,13 +1458,29 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Skip Delivery Note", "label": "Skip Delivery Note",
"print_hide": 1 "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", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-30 13:59:18.628077", "modified": "2021-01-20 23:40:39.929296",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@ -399,6 +399,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
} }
}, },
batch_no: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn);
},
qty: function(doc, cdt, cdn) { qty: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn); this._super(doc, cdt, cdn);

View File

@ -178,6 +178,13 @@ def create_lead_for_item_inquiry(lead, subject, message):
lead_doc.update(lead) lead_doc.update(lead)
lead_doc.set('lead_owner', '') 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: try:
lead_doc.save(ignore_permissions=True) lead_doc.save(ignore_permissions=True)
except frappe.exceptions.DuplicateEntryError: except frappe.exceptions.DuplicateEntryError:

View File

@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({
handle_move_add($(this), "Add") 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) { function handle_move_add(element, action) {
let item = unescape(element.attr('data-item')); let item = unescape(element.attr('data-item'));
let warehouse = unescape(element.attr('data-warehouse')); let warehouse = unescape(element.attr('data-warehouse'));
@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({
// more // more
this.content.find('.btn-more').on('click', function() { this.content.find('.btn-more').on('click', function() {
me.start += 20; me.start += me.page_length;
me.refresh(); me.refresh();
}); });
@ -69,33 +79,43 @@ erpnext.stock.ItemDashboard = Class.extend({
this.before_refresh(); 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; var me = this;
frappe.call({ frappe.call({
method: 'erpnext.stock.dashboard.item_dashboard.get_data', method: this.method,
args: { args: 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,
},
callback: function(r) { callback: function(r) {
me.render(r.message); me.render(r.message);
} }
}); });
}, },
render: function(data) { render: function(data) {
if(this.start===0) { if (this.start===0) {
this.max_count = 0; this.max_count = 0;
this.result.empty(); 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; this.max_count = this.max_count;
// show more button // show more button
if(data && data.length===21) { if (data && data.length===(this.page_length + 1)) {
this.content.find('.more').removeClass('hidden'); this.content.find('.more').removeClass('hidden');
// remove the last element // remove the last element
@ -106,12 +126,17 @@ erpnext.stock.ItemDashboard = Class.extend({
// If not any stock in any warehouses provide a message to end user // If not any stock in any warehouses provide a message to end user
if (context.data.length > 0) { 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 { } else {
var message = __("Currently no stock available in any warehouse"); var message = __("No Stock Available Currently");
$(`<span class='text-muted small'> ${message} </span>`).appendTo(this.result); 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) { get_item_dashboard_data: function(data, max_count, show_item) {
if(!max_count) max_count = 0; if(!max_count) max_count = 0;
if(!data) data = []; if(!data) data = [];
@ -128,8 +153,8 @@ erpnext.stock.ItemDashboard = Class.extend({
d.total_reserved, max_count); d.total_reserved, max_count);
}); });
var can_write = 0; let can_write = 0;
if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) {
can_write = 1; can_write = 1;
} }
@ -138,9 +163,27 @@ erpnext.stock.ItemDashboard = Class.extend({
max_count: max_count, max_count: max_count,
can_write:can_write, can_write:can_write,
show_item: show_item || false 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) { erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({

View File

@ -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

View File

@ -8,12 +8,12 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Stock Transactions", "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, "hidden": 0,
"label": "Stock Reports", "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, "hidden": 0,
@ -58,7 +58,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Stock", "label": "Stock",
"modified": "2020-12-02 15:47:41.532942", "modified": "2020-12-08 15:47:41.532942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock", "name": "Stock",

View File

@ -8,6 +8,8 @@ import unittest
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint, flt 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): class TestBatch(unittest.TestCase):
def test_item_has_batch_enabled(self): def test_item_has_batch_enabled(self):
@ -252,6 +254,72 @@ class TestBatch(unittest.TestCase):
return batch 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): def make_new_batch(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -95,13 +95,19 @@ frappe.ui.form.on("Delivery Note", {
frm.page.set_inner_btn_group_as_primary(__('Create')); frm.page.set_inner_btn_group_as_primary(__('Create'));
} }
if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) { if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
frm.add_custom_button(__('Purchase Receipt'), function() { let internal = me.frm.doc.is_internal_customer;
frappe.model.open_mapped_doc({ if (internal) {
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" :
frm: frm, "Inter Company Purchase Receipt";
})
}, __('Create')); me.frm.add_custom_button(button_label, function() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
frm: frm,
});
}, __('Create'));
}
} }
} }
}); });
@ -297,15 +303,6 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
} }
}) })
}, },
to_warehouse: function() {
let packed_items_table = this.frm.doc["packed_items"];
this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse);
if (packed_items_table && packed_items_table.length) {
this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse);
}
}
}); });
$.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm})); $.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm}));

View File

@ -53,7 +53,7 @@
"sec_warehouse", "sec_warehouse",
"set_warehouse", "set_warehouse",
"col_break_warehouse", "col_break_warehouse",
"to_warehouse", "set_target_warehouse",
"items_section", "items_section",
"scan_barcode", "scan_barcode",
"items", "items",
@ -117,6 +117,7 @@
"source", "source",
"column_break5", "column_break5",
"is_internal_customer", "is_internal_customer",
"represents_company",
"inter_company_reference", "inter_company_reference",
"per_billed", "per_billed",
"customer_group", "customer_group",
@ -502,18 +503,6 @@
"fieldname": "col_break_warehouse", "fieldname": "col_break_warehouse",
"fieldtype": "Column Break" "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", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -1261,13 +1250,34 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 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", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-30 12:54:45.407289", "modified": "2020-12-26 17:07:59.194403",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@ -664,7 +664,8 @@ def make_inter_company_purchase_receipt(source_name, target_doc=None):
return make_inter_company_transaction("Delivery Note", source_name, target_doc) return make_inter_company_transaction("Delivery Note", source_name, target_doc)
def make_inter_company_transaction(doctype, source_name, target_doc=None): 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': if doctype == 'Delivery Note':
source_doc = frappe.get_doc(doctype, source_name) source_doc = frappe.get_doc(doctype, source_name)
@ -682,6 +683,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.run_method("set_missing_values") target.run_method("set_missing_values")
set_purchase_references(target)
if target.doctype == 'Purchase Receipt': if target.doctype == 'Purchase Receipt':
master_doctype = 'Purchase Taxes and Charges Template' master_doctype = 'Purchase Taxes and Charges Template'
@ -697,21 +699,35 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if target_doc.doctype == 'Purchase Receipt': if target_doc.doctype == 'Purchase Receipt':
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.supplier = details.get("party") 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.buying_price_list = source_doc.selling_price_list
target_doc.is_internal_supplier = 1 target_doc.is_internal_supplier = 1
target_doc.inter_company_reference = source_doc.name 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: else:
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.customer = details.get("party") target_doc.customer = details.get("party")
target_doc.company_address = source_doc.supplier_address 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.selling_price_list = source_doc.buying_price_list
target_doc.is_internal_customer = 1 target_doc.is_internal_customer = 1
target_doc.inter_company_reference = source_doc.name 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: {
"doctype": target_doctype, "doctype": target_doctype,
"postprocess": update_details, "postprocess": update_details,
@ -722,7 +738,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
doctype +" Item": { doctype +" Item": {
"doctype": target_doctype + " Item", "doctype": target_doctype + " Item",
"field_map": { "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": [ "field_no_map": [
"warehouse" "warehouse"

View File

@ -458,7 +458,7 @@
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "From Warehouse", "label": "Warehouse",
"oldfieldname": "warehouse", "oldfieldname": "warehouse",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Warehouse", "options": "Warehouse",
@ -467,11 +467,12 @@
"width": "100px" "width": "100px"
}, },
{ {
"depends_on": "eval:parent.is_internal_customer",
"fieldname": "target_warehouse", "fieldname": "target_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Customer Warehouse (Optional)", "label": "Target Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1 "print_hide": 1
@ -748,7 +749,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 19:59:27.119856", "modified": "2020-12-26 17:31:27.029803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -384,7 +384,10 @@ $.extend(erpnext.item, {
<a href="#stock-balance">' + __("Stock Levels") + '</a></h5>'); <a href="#stock-balance">' + __("Stock Levels") + '</a></h5>');
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
parent: section, 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(); erpnext.item.item_dashboard.refresh();
}); });

View File

@ -106,9 +106,9 @@
"item_tax_section_break", "item_tax_section_break",
"taxes", "taxes",
"inspection_criteria", "inspection_criteria",
"quality_inspection_template",
"inspection_required_before_purchase", "inspection_required_before_purchase",
"inspection_required_before_delivery", "inspection_required_before_delivery",
"quality_inspection_template",
"manufacturing", "manufacturing",
"default_bom", "default_bom",
"is_sub_contracted_item", "is_sub_contracted_item",
@ -814,7 +814,6 @@
"label": "Inspection Required before Delivery" "label": "Inspection Required before Delivery"
}, },
{ {
"depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)",
"fieldname": "quality_inspection_template", "fieldname": "quality_inspection_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Quality Inspection Template", "label": "Quality Inspection Template",
@ -1069,7 +1068,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 1, "max_attachments": 1,
"modified": "2020-08-07 14:24:58.384992", "modified": "2021-01-25 20:49:50.222976",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -15,5 +15,13 @@ frappe.ui.form.on("Item Price", {
frm.set_df_property("bulk_import_help", "options", frm.set_df_property("bulk_import_help", "options",
'<a href="#data-import-tool/Item Price">' + __("Import in Bulk") + '</a>'); '<a href="#data-import-tool/Item Price">' + __("Import in Bulk") + '</a>');
frm.set_query('batch_no', function() {
return {
filters: {
'item': frm.doc.item_code
}
}
});
} }
}); });

View File

@ -18,6 +18,7 @@
"price_list", "price_list",
"customer", "customer",
"supplier", "supplier",
"batch_no",
"column_break_3", "column_break_3",
"buying", "buying",
"selling", "selling",
@ -47,31 +48,41 @@
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Item", "options": "Item",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "UOM", "label": "UOM",
"options": "UOM" "options": "UOM",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "Quantity that must be bought or sold per UOM", "description": "Quantity that must be bought or sold per UOM",
"fieldname": "packing_unit", "fieldname": "packing_unit",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Packing Unit" "label": "Packing Unit",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Item Name", "label": "Item Name",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "item_code.brand", "fetch_from": "item_code.brand",
@ -79,19 +90,25 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
"label": "Brand", "label": "Brand",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_description", "fieldname": "item_description",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Item Description", "label": "Item Description",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list_details", "fieldname": "price_list_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Price List", "label": "Price List",
"options": "fa fa-tags" "options": "fa fa-tags",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list", "fieldname": "price_list",
@ -100,7 +117,9 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Price List", "label": "Price List",
"options": "Price List", "options": "Price List",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
@ -108,37 +127,49 @@
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer" "options": "Customer",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.buying == 1", "depends_on": "eval:doc.buying == 1",
"fieldname": "supplier", "fieldname": "supplier",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier", "label": "Supplier",
"options": "Supplier" "options": "Supplier",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "buying", "fieldname": "buying",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Buying", "label": "Buying",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "selling", "fieldname": "selling",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Selling", "label": "Selling",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_details", "fieldname": "item_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"options": "fa fa-tag" "options": "fa fa-tag",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
@ -146,11 +177,15 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency", "options": "Currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "col_br_1", "fieldname": "col_br_1",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list_rate", "fieldname": "price_list_rate",
@ -162,53 +197,80 @@
"oldfieldname": "ref_rate", "oldfieldname": "ref_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_15", "fieldname": "section_break_15",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "Today", "default": "Today",
"fieldname": "valid_from", "fieldname": "valid_from",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid From" "label": "Valid From",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "lead_time_days", "fieldname": "lead_time_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Lead Time in days" "label": "Lead Time in days",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Upto",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_24", "fieldname": "section_break_24",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "note", "fieldname": "note",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Note" "label": "Note",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "reference", "fieldname": "reference",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "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", "icon": "fa fa-flag",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-07-06 22:31:32.943475", "modified": "2020-12-08 18:12:15.395772",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Price", "name": "Item Price",

View File

@ -54,7 +54,8 @@ class ItemPrice(Document):
"valid_upto", "valid_upto",
"packing_unit", "packing_unit",
"customer", "customer",
"supplier",]: "supplier",
"batch_no"]:
if self.get(field): if self.get(field):
conditions += " and {0} = %({0})s ".format(field) conditions += " and {0} = %({0})s ".format(field)
else: else:
@ -68,7 +69,7 @@ class ItemPrice(Document):
self.as_dict(),) self.as_dict(),)
if price_list_rate: 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): def before_save(self):
if self.selling: if self.selling:

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2013-02-22 01:28:02", "creation": "2013-02-22 01:28:02",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
@ -29,6 +30,8 @@
"options": "Item", "options": "Item",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"show_days": 1,
"show_seconds": 1,
"width": "100px" "width": "100px"
}, },
{ {
@ -41,6 +44,8 @@
"print_width": "300px", "print_width": "300px",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"show_days": 1,
"show_seconds": 1,
"width": "120px" "width": "120px"
}, },
{ {
@ -50,7 +55,9 @@
"no_copy": 1, "no_copy": 1,
"options": "Purchase Invoice\nPurchase Receipt", "options": "Purchase Invoice\nPurchase Receipt",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "receipt_document", "fieldname": "receipt_document",
@ -59,25 +66,33 @@
"no_copy": 1, "no_copy": 1,
"options": "receipt_document_type", "options": "receipt_document_type",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "col_break2", "fieldname": "col_break2",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty", "label": "Qty",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate", "label": "Rate",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amount", "fieldname": "amount",
@ -88,14 +103,19 @@
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "applicable_charges", "fieldname": "applicable_charges",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Applicable Charges", "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", "fieldname": "purchase_receipt_item",
@ -104,22 +124,30 @@
"label": "Purchase Receipt Item", "label": "Purchase Receipt Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions" "label": "Accounting Dimensions",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
@ -128,12 +156,15 @@
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"label": "Is Fixed Asset", "label": "Is Fixed Asset",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2020-09-18 17:26:09.703215", "links": [],
"modified": "2021-01-25 23:09:23.322282",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Item", "name": "Landed Cost Item",

View File

@ -6,8 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"expense_account", "expense_account",
"account_currency",
"exchange_rate",
"description", "description",
"col_break3", "col_break3",
"base_amount",
"amount" "amount"
], ],
"fields": [ "fields": [
@ -28,7 +31,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Amount", "label": "Amount",
"options": "Company:company:default_currency", "options": "account_currency",
"reqd": 1 "reqd": 1
}, },
{ {
@ -38,13 +41,33 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Expense Account", "label": "Expense Account",
"mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"options": "Account", "options": "Account"
"print_hide": 1 },
{
"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, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-04 00:22:14.373312", "modified": "2020-12-26 01:07:23.233604",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Taxes and Charges", "name": "Landed Cost Taxes and Charges",

View File

@ -1,6 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %};
frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock");
@ -29,20 +30,9 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
this.frm.add_fetch("receipt_document", "supplier", "supplier"); this.frm.add_fetch("receipt_document", "supplier", "supplier");
this.frm.add_fetch("receipt_document", "posting_date", "posting_date"); this.frm.add_fetch("receipt_document", "posting_date", "posting_date");
this.frm.add_fetch("receipt_document", "base_grand_total", "grand_total"); 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 = var help_content =
`<br><br> `<br><br>
<table class="table table-bordered" style="background-color: #f9f9f9;"> <table class="table table-bordered" style="background-color: #f9f9f9;">
@ -72,6 +62,11 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
</table>`; </table>`;
set_field_options("landed_cost_help", help_content); 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() { get_items_from_purchase_receipts: function() {
@ -97,34 +92,36 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
set_total_taxes_and_charges: function() { set_total_taxes_and_charges: function() {
var total_taxes_and_charges = 0.0; var total_taxes_and_charges = 0.0;
$.each(this.frm.doc.taxes || [], function(i, d) { $.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() { set_applicable_charges_for_item: function() {
var me = this; var me = this;
if(this.frm.doc.taxes.length) { if(this.frm.doc.taxes.length) {
var total_item_cost = 0.0; var total_item_cost = 0.0;
var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase(); 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; if (based_on != 'distribute manually') {
$.each(this.frm.doc.items || [], function(i, item) { $.each(this.frm.doc.items || [], function(i, d) {
item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) total_item_cost += flt(d[based_on])
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 total_charges = 0.0;
var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) $.each(this.frm.doc.items || [], function(i, item) {
this.frm.doc.items.slice(-1)[0].applicable_charges += diff 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) { distribute_charges_based_on: function (frm) {
@ -134,7 +131,16 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
items_remove: () => { items_remove: () => {
this.trigger('set_applicable_charges_for_item'); this.trigger('set_applicable_charges_for_item');
} }
}); });
cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher); 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);
}
});

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2014-07-11 11:33:42.547339", "creation": "2014-07-11 11:33:42.547339",
"doctype": "DocType", "doctype": "DocType",
@ -7,6 +8,9 @@
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company", "company",
"column_break_2",
"posting_date",
"section_break_5",
"purchase_receipts", "purchase_receipts",
"purchase_receipt_items", "purchase_receipt_items",
"get_items_from_purchase_receipts", "get_items_from_purchase_receipts",
@ -30,7 +34,9 @@
"options": "MAT-LCV-.YYYY.-", "options": "MAT-LCV-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
@ -40,24 +46,32 @@
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"remember_last_selected_value": 1, "remember_last_selected_value": 1,
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "purchase_receipts", "fieldname": "purchase_receipts",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Purchase Receipts", "label": "Purchase Receipts",
"options": "Landed Cost Purchase Receipt", "options": "Landed Cost Purchase Receipt",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "purchase_receipt_items", "fieldname": "purchase_receipt_items",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Purchase Receipt Items" "label": "Purchase Receipt Items",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "get_items_from_purchase_receipts", "fieldname": "get_items_from_purchase_receipts",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Items From Purchase Receipts" "label": "Get Items From Purchase Receipts",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "items", "fieldname": "items",
@ -65,42 +79,56 @@
"label": "Purchase Receipt Items", "label": "Purchase Receipt Items",
"no_copy": 1, "no_copy": 1,
"options": "Landed Cost Item", "options": "Landed Cost Item",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "sec_break1", "fieldname": "sec_break1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Applicable Charges" "label": "Applicable Charges",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "taxes", "fieldname": "taxes",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Taxes and Charges", "label": "Taxes and Charges",
"options": "Landed Cost Taxes and Charges", "options": "Landed Cost Taxes and Charges",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "total_taxes_and_charges", "fieldname": "total_taxes_and_charges",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Taxes and Charges", "label": "Total Taxes and Charges (Company Currency)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "col_break1", "fieldname": "col_break1",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "distribute_charges_based_on", "fieldname": "distribute_charges_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Distribute Charges Based On", "label": "Distribute Charges Based On",
"options": "Qty\nAmount", "options": "Qty\nAmount\nDistribute Manually",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@ -109,21 +137,51 @@
"no_copy": 1, "no_copy": 1,
"options": "Landed Cost Voucher", "options": "Landed Cost Voucher",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "sec_break2", "fieldname": "sec_break2",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "landed_cost_help", "fieldname": "landed_cost_help",
"fieldtype": "HTML", "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", "icon": "icon-usd",
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-11-21 15:34:10.846093", "links": [],
"modified": "2021-01-25 23:07:30.468423",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Voucher", "name": "Landed Cost Voucher",

View File

@ -9,6 +9,7 @@ from frappe.model.meta import get_field_precision
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.accounts.doctype.account.account import get_account_currency 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): class LandedCostVoucher(Document):
def get_items_from_purchase_receipts(self): def get_items_from_purchase_receipts(self):
@ -39,13 +40,15 @@ class LandedCostVoucher(Document):
def validate(self): def validate(self):
self.check_mandatory() self.check_mandatory()
self.validate_purchase_receipts()
init_landed_taxes_and_totals(self)
self.set_total_taxes_and_charges()
if not self.get("items"): if not self.get("items"):
self.get_items_from_purchase_receipts() self.get_items_from_purchase_receipts()
else:
self.validate_applicable_charges_for_item() self.set_applicable_charges_on_item()
self.validate_purchase_receipts() self.validate_applicable_charges_for_item()
self.validate_expense_accounts()
self.set_total_taxes_and_charges()
def check_mandatory(self): def check_mandatory(self):
if not self.get("purchase_receipts"): if not self.get("purchase_receipts"):
@ -73,21 +76,37 @@ class LandedCostVoucher(Document):
frappe.throw(_("Row {0}: Cost center is required for an item {1}") frappe.throw(_("Row {0}: Cost center is required for an item {1}")
.format(item.idx, item.item_code)) .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): 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): def validate_applicable_charges_for_item(self):
based_on = self.distribute_charges_based_on.lower() 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: if not total:
frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on))
@ -153,13 +172,13 @@ class LandedCostVoucher(Document):
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus']) 'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty: if not docs or len(docs) != item.qty:
frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document) frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format(
+ _('Please create or link {0} Assets with respective document.').format(item.qty)) item.receipt_document, item.qty))
if docs: if docs:
for d in docs: for d in docs:
if d.docstatus == 1: if d.docstatus == 1:
frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.') frappe.throw(_('{2} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> from table to continue.').format(
.format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code))) item.receipt_document, item.item_code, item.receipt_document_type))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"): for item in receipt_document.get("items"):

View File

@ -10,6 +10,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
import get_gl_entries, test_records as pr_test_records, make_purchase_receipt 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.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 get_inventory_account
from erpnext.accounts.doctype.account.test_account import create_account
class TestLandedCostVoucher(unittest.TestCase): class TestLandedCostVoucher(unittest.TestCase):
def test_landed_cost_voucher(self): def test_landed_cost_voucher(self):
@ -162,8 +163,8 @@ class TestLandedCostVoucher(unittest.TestCase):
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
self.assertEqual(lcv.items[0].applicable_charges, 41.07) self.assertEqual(flt(lcv.items[0].applicable_charges, 2), 41.07)
self.assertEqual(lcv.items[2].applicable_charges, 41.08) self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08)
def test_multiple_landed_cost_voucher_against_pr(self): def test_multiple_landed_cost_voucher_against_pr(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
@ -206,6 +207,46 @@ class TestLandedCostVoucher(unittest.TestCase):
self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100)
self.assertEqual(pr.items[1].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): def make_landed_cost_voucher(** args):
args = frappe._dict(args) args = frappe._dict(args)
ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document)

View File

@ -1,9 +1,10 @@
frappe.listview_settings['Material Request'] = { frappe.listview_settings['Material Request'] = {
add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"],
get_indicator: function(doc) { 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"]; 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") { if (doc.transfer_status == "Not Started") {
return [__("Not Started"), "orange"]; return [__("Not Started"), "orange"];
} else if (doc.transfer_status == "In Transit") { } else if (doc.transfer_status == "In Transit") {
@ -11,14 +12,14 @@ frappe.listview_settings['Material Request'] = {
} else if (doc.transfer_status == "Completed") { } else if (doc.transfer_status == "Completed") {
return [__("Completed"), "green"]; 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"]; 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"]; return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
} else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) { } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) {
if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) { 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"]; 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"]; return [__("Received"), "green", "per_received,=,100"];
} else if (doc.material_request_type == "Purchase") { } else if (doc.material_request_type == "Purchase") {
return [__("Ordered"), "green", "per_ordered,=,100"]; return [__("Ordered"), "green", "per_ordered,=,100"];

View File

@ -216,6 +216,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
}); });
}, },
apply_putaway_rule: function() {
if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm);
}
}); });
// for backward compatibility: combine new and previous states // for backward compatibility: combine new and previous states

View File

@ -21,6 +21,7 @@
"posting_date", "posting_date",
"posting_time", "posting_time",
"set_posting_time", "set_posting_time",
"apply_putaway_rule",
"is_return", "is_return",
"return_against", "return_against",
"section_addresses", "section_addresses",
@ -47,6 +48,7 @@
"set_warehouse", "set_warehouse",
"rejected_warehouse", "rejected_warehouse",
"col_break_warehouse", "col_break_warehouse",
"set_from_warehouse",
"is_subcontracted", "is_subcontracted",
"supplier_warehouse", "supplier_warehouse",
"items_section", "items_section",
@ -114,6 +116,7 @@
"per_returned", "per_returned",
"is_internal_supplier", "is_internal_supplier",
"inter_company_reference", "inter_company_reference",
"represents_company",
"subscription_detail", "subscription_detail",
"auto_repeat", "auto_repeat",
"printing_settings", "printing_settings",
@ -1086,7 +1089,9 @@
"fieldname": "inter_company_reference", "fieldname": "inter_company_reference",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Inter Company Reference", "label": "Inter Company Reference",
"no_copy": 1,
"options": "Delivery Note", "options": "Delivery Note",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@ -1106,6 +1111,12 @@
"label": "Billing Address", "label": "Billing Address",
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fieldname": "apply_putaway_rule",
"fieldtype": "Check",
"label": "Apply Putaway Rule"
},
{ {
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal",
"fieldname": "per_returned", "fieldname": "per_returned",
@ -1114,13 +1125,29 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 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", "icon": "fa fa-truck",
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-30 12:54:23.278500", "modified": "2020-12-26 20:49:39.106049",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -83,6 +83,12 @@ class PurchaseReceipt(BuyingController):
} }
]) ])
def before_validate(self):
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
if self.get("items") and self.apply_putaway_rule and not self.get("is_return"):
apply_putaway_rule(self.doctype, self.get("items"), self.company)
def validate(self): def validate(self):
self.validate_posting_time() self.validate_posting_time()
super(PurchaseReceipt, self).validate() super(PurchaseReceipt, self).validate()
@ -103,6 +109,7 @@ class PurchaseReceipt(BuyingController):
if getdate(self.posting_date) > getdate(nowdate()): if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date")) throw(_("Posting Date cannot be future date"))
def validate_cwip_accounts(self): def validate_cwip_accounts(self):
for item in self.get('items'): for item in self.get('items'):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
@ -281,12 +288,15 @@ class PurchaseReceipt(BuyingController):
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if d.landed_cost_voucher_amount and landed_cost_entries: if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): 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({ gl_entries.append(self.get_gl_dict({
"account": account, "account": account,
"account_currency": account_currency,
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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 "project": d.project
}, item=d)) }, item=d))
@ -721,7 +731,13 @@ def get_item_account_wise_additional_cost(purchase_document):
for lcv in landed_cost_vouchers: for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) 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 total_item_cost = 0
for item in landed_cost_voucher_doc.items: for item in landed_cost_voucher_doc.items:
@ -731,9 +747,16 @@ def get_item_account_wise_additional_cost(purchase_document):
if item.receipt_document == purchase_document: if item.receipt_document == purchase_document:
for account in landed_cost_voucher_doc.taxes: for account in landed_cost_voucher_doc.taxes:
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) 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)].setdefault(account.expense_account, {
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][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 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 return item_account_wise_cost

View File

@ -1014,6 +1014,7 @@ def make_purchase_receipt(**args):
pr.currency = args.currency or "INR" pr.currency = args.currency or "INR"
pr.is_return = args.is_return pr.is_return = args.is_return
pr.return_against = args.return_against pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule
qty = args.qty or 5 qty = args.qty or 5
received_qty = args.received_qty or qty received_qty = args.received_qty or qty
rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty)
@ -1029,6 +1030,7 @@ def make_purchase_receipt(**args):
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
"rate": args.rate if args.rate != None else 50, "rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0, "conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no, "serial_no": args.serial_no,
"stock_uom": args.stock_uom or "_Test UOM", "stock_uom": args.stock_uom or "_Test UOM",
"uom": uom, "uom": uom,

View File

@ -76,6 +76,8 @@
"purchase_order_item", "purchase_order_item",
"material_request_item", "material_request_item",
"purchase_receipt_item", "purchase_receipt_item",
"delivery_note_item",
"putaway_rule",
"section_break_45", "section_break_45",
"allow_zero_valuation_rate", "allow_zero_valuation_rate",
"bom", "bom",
@ -818,11 +820,12 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse", "fieldname": "from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Supplier Warehouse", "label": "From Warehouse",
"options": "Warehouse" "options": "Warehouse"
}, },
{ {
@ -839,6 +842,15 @@
"fieldname": "image_column", "fieldname": "image_column",
"fieldtype": "Column Break" "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", "fieldname": "tracking_section",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@ -861,12 +873,20 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Received Qty in Stock UOM", "label": "Received Qty in Stock UOM",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "delivery_note_item",
"fieldtype": "Data",
"label": "Delivery Note Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 10:00:38.204294", "modified": "2020-12-26 16:50:56.479347",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@ -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) {
// }
});

View File

@ -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
}

View File

@ -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 ""

View File

@ -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'
}
]
};

View File

@ -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

View File

@ -3,8 +3,18 @@
frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock");
frappe.provide("erpnext.accounts.dimensions"); frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %};
frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry', {
setup: function(frm) { 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() { frm.set_query('work_order', function() {
return { return {
filters: [ filters: [
@ -87,15 +97,6 @@ frappe.ui.form.on('Stock Entry', {
} }
}); });
frm.set_query("expense_account", "additional_costs", function() {
return {
query: "erpnext.controllers.queries.tax_account_query",
filters: {
"account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"],
"company": frm.doc.company
}
};
});
frm.add_fetch("bom_no", "inspection_required", "inspection_required"); frm.add_fetch("bom_no", "inspection_required", "inspection_required");
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
@ -550,7 +551,7 @@ frappe.ui.form.on('Stock Entry', {
calculate_total_additional_costs: function(frm) { calculate_total_additional_costs: function(frm) {
const total_additional_costs = frappe.utils.sum( 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", frm.set_value("total_additional_costs",
@ -583,8 +584,12 @@ frappe.ui.form.on('Stock Entry', {
} }
}); });
} }
},
apply_putaway_rule: function (frm) {
if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose);
} }
}) });
frappe.ui.form.on('Stock Entry Detail', { frappe.ui.form.on('Stock Entry Detail', {
qty: function(frm, cdt, cdn) { qty: function(frm, cdt, cdn) {
@ -725,8 +730,18 @@ var validate_sample_quantity = function(frm, cdt, cdn) {
}; };
frappe.ui.form.on('Landed Cost Taxes and Charges', { frappe.ui.form.on('Landed Cost Taxes and Charges', {
amount: function(frm) { amount: function(frm, cdt, cdn) {
frm.events.calculate_amount(frm); frm.events.set_base_amount(frm, cdt, cdn);
// Adding this check because same table in used in LCV
// This causes an error if you try to post an LCV immediately after a Stock Entry
if (frm.doc.doctype == 'Stock Entry') {
frm.events.calculate_amount(frm);
}
},
expense_account: function(frm, cdt, cdn) {
frm.events.set_account_currency(frm, cdt, cdn);
} }
}); });
@ -774,15 +789,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
} }
} }
this.frm.set_indicator_formatter('item_code',
function(doc) {
if (!doc.s_warehouse) {
return 'blue';
} else {
return (doc.qty<=doc.actual_qty) ? "green" : "orange"
}
})
this.frm.add_fetch("purchase_order", "supplier", "supplier"); this.frm.add_fetch("purchase_order", "supplier", "supplier");
frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' }

View File

@ -27,6 +27,7 @@
"set_posting_time", "set_posting_time",
"inspection_required", "inspection_required",
"from_bom", "from_bom",
"apply_putaway_rule",
"sb1", "sb1",
"bom_no", "bom_no",
"fg_completed_qty", "fg_completed_qty",
@ -640,6 +641,13 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Add to Transit", "label": "Add to Transit",
"no_copy": 1 "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", "icon": "fa fa-file-text",
@ -647,7 +655,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-09-09 12:59:02.508943", "modified": "2020-12-09 14:58:13.267321",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -19,6 +19,7 @@ from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos from erpnext.stock.doctype.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.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
import json import json
from six import string_types, itervalues, iteritems from six import string_types, itervalues, iteritems
@ -42,6 +43,14 @@ class StockEntry(StockController):
for item in self.get("items"): for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse)) 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): def validate(self):
self.pro_doc = frappe._dict() self.pro_doc = frappe._dict()
if self.work_order: if self.work_order:
@ -79,6 +88,7 @@ class StockEntry(StockController):
self.validate_serialized_batch() self.validate_serialized_batch()
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount() self.calculate_rate_and_amount()
self.validate_putaway_capacity()
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
@ -186,7 +196,7 @@ class StockEntry(StockController):
and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1)
amount = amount[0][0] if amount else 0 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 from
`tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed
where where
@ -436,6 +446,7 @@ class StockEntry(StockController):
def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): 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) self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
init_landed_taxes_and_totals(self)
self.distribute_additional_costs() self.distribute_additional_costs()
self.update_valuation_rate() self.update_valuation_rate()
self.set_total_incoming_outgoing_value() self.set_total_incoming_outgoing_value()
@ -524,7 +535,7 @@ class StockEntry(StockController):
if not any([d.item_code for d in self.items if d.t_warehouse]): if not any([d.item_code for d in self.items if d.t_warehouse]):
self.additional_costs = [] 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"): 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]) incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
@ -764,13 +775,19 @@ class StockEntry(StockController):
for d in self.get("items"): for d in self.get("items"):
if d.t_warehouse: if d.t_warehouse:
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) 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 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 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: if item_account_wise_additional_cost:
for d in self.get("items"): for d in self.get("items"):
for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})):
@ -781,7 +798,8 @@ class StockEntry(StockController):
"against": d.expense_account, "against": d.expense_account,
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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)) }, item=d))
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
@ -789,7 +807,7 @@ class StockEntry(StockController):
"against": account, "against": account,
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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)) }, item=d))
return process_gl_map(gl_entries) return process_gl_map(gl_entries)

View File

@ -53,6 +53,8 @@ def make_stock_entry(**args):
args.target = args.to_warehouse args.target = args.to_warehouse
if args.item_code: if args.item_code:
args.item = 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 isinstance(args.qty, string_types):
if '.' in args.qty: if '.' in args.qty:
@ -118,7 +120,8 @@ def make_stock_entry(**args):
"t_warehouse": args.target, "t_warehouse": args.target,
"qty": args.qty, "qty": args.qty,
"basic_rate": args.rate or args.basic_rate, "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, "serial_no": args.serial_no,
'batch_no': args.batch_no, 'batch_no': args.batch_no,
'cost_center': args.cost_center, 'cost_center': args.cost_center,

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-03-29 18:22:12", "creation": "2013-03-29 18:22:12",
"doctype": "DocType", "doctype": "DocType",
@ -65,6 +66,7 @@
"against_stock_entry", "against_stock_entry",
"ste_detail", "ste_detail",
"po_detail", "po_detail",
"putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
"quality_inspection" "quality_inspection"
@ -495,6 +497,16 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Basic Rate Manually" "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", "fieldname": "quantity_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -526,7 +538,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-23 17:55:03.384138", "modified": "2020-12-30 15:00:44.489442",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@ -30,6 +30,7 @@ class StockReconciliation(StockController):
self.validate_data() self.validate_data()
self.validate_expense_account() self.validate_expense_account()
self.set_total_qty_and_amount() self.set_total_qty_and_amount()
self.validate_putaway_capacity()
if self._action=="submit": if self._action=="submit":
self.make_batches('warehouse') self.make_batches('warehouse')

View File

@ -16,6 +16,7 @@
"action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_not_submitted",
"show_barcode_field", "show_barcode_field",
"clean_description_html", "clean_description_html",
"disable_serial_no_and_batch_selector",
"section_break_7", "section_break_7",
"auto_insert_price_list_rate_if_missing", "auto_insert_price_list_rate_if_missing",
"allow_negative_stock", "allow_negative_stock",
@ -227,6 +228,12 @@
"fieldname": "control_historical_stock_transactions_section", "fieldname": "control_historical_stock_transactions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Control Historical Stock Transactions" "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", "icon": "icon-cog",
@ -234,7 +241,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-12-29 12:53:31.162247", "modified": "2021-01-18 13:15:38.352796",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@ -674,6 +674,8 @@ def get_item_price(args, item_code, ignore_party=False):
and price_list=%(price_list)s and price_list=%(price_list)s
and ifnull(uom, '') in ('', %(uom)s)""" and ifnull(uom, '') in ('', %(uom)s)"""
conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)"
if not ignore_party: if not ignore_party:
if args.get("customer"): if args.get("customer"):
conditions += " and customer=%(customer)s" conditions += " and customer=%(customer)s"
@ -692,7 +694,7 @@ def get_item_price(args, item_code, ignore_party=False):
return frappe.db.sql(""" select name, price_list_rate, uom return frappe.db.sql(""" select name, price_list_rate, uom
from `tabItem Price` {conditions} 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): def get_price_list_rate_for(args, item_code):
""" """
@ -711,6 +713,7 @@ def get_price_list_rate_for(args, item_code):
"uom": args.get('uom'), "uom": args.get('uom'),
"transaction_date": args.get('transaction_date'), "transaction_date": args.get('transaction_date'),
"posting_date": args.get('posting_date'), "posting_date": args.get('posting_date'),
"batch_no": args.get('batch_no')
} }
item_price_data = 0 item_price_data = 0

View File

@ -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)));
}
});
});

View File

@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
frappe.require('assets/js/item-dashboard.min.js', function() { frappe.require('assets/js/item-dashboard.min.js', function() {
page.item_dashboard = new erpnext.stock.ItemDashboard({ page.item_dashboard = new erpnext.stock.ItemDashboard({
parent: page.main, parent: page.main,
page_length: 20,
method: 'erpnext.stock.dashboard.item_dashboard.get_data',
template: 'item_dashboard_list'
}) })
page.item_dashboard.before_refresh = function() { page.item_dashboard.before_refresh = function() {

View File

@ -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 %}

Some files were not shown because too many files have changed in this diff Show More