Merge branch 'develop' of https://github.com/frappe/erpnext into asset_purchase_receipt_gl
This commit is contained in:
commit
726fba61f3
@ -32,6 +32,7 @@
|
|||||||
"column_break_19",
|
"column_break_19",
|
||||||
"add_taxes_from_item_tax_template",
|
"add_taxes_from_item_tax_template",
|
||||||
"book_tax_discount_loss",
|
"book_tax_discount_loss",
|
||||||
|
"round_row_wise_tax",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
"show_inclusive_tax_in_print",
|
"show_inclusive_tax_in_print",
|
||||||
"show_taxes_as_table_in_print",
|
"show_taxes_as_table_in_print",
|
||||||
@ -414,6 +415,13 @@
|
|||||||
"fieldname": "ignore_account_closing_balance",
|
"fieldname": "ignore_account_closing_balance",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Account Closing Balance"
|
"label": "Ignore Account Closing Balance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Tax Amount will be rounded on a row(items) level",
|
||||||
|
"fieldname": "round_row_wise_tax",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Round Tax Amount Row-wise"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@ -421,7 +429,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-27 15:05:34.000264",
|
"modified": "2023-08-28 00:12:02.740633",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"currency_and_price_list",
|
"currency_and_price_list",
|
||||||
"currency",
|
"currency",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
|
"use_transaction_date_exchange_rate",
|
||||||
"column_break2",
|
"column_break2",
|
||||||
"buying_price_list",
|
"buying_price_list",
|
||||||
"price_list_currency",
|
"price_list_currency",
|
||||||
@ -1588,13 +1589,20 @@
|
|||||||
"label": "Repost Required",
|
"label": "Repost Required",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "use_transaction_date_exchange_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Transaction Date Exchange Rate",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-01 21:01:47.282533",
|
"modified": "2023-10-16 16:24:51.886231",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||||
"disable_last_purchase_rate",
|
"disable_last_purchase_rate",
|
||||||
"show_pay_button",
|
"show_pay_button",
|
||||||
|
"use_transaction_date_exchange_rate",
|
||||||
"subcontract",
|
"subcontract",
|
||||||
"backflush_raw_materials_of_subcontract_based_on",
|
"backflush_raw_materials_of_subcontract_based_on",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
@ -164,6 +165,13 @@
|
|||||||
"fieldname": "over_order_allowance",
|
"fieldname": "over_order_allowance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Over Order Allowance (%)"
|
"label": "Over Order Allowance (%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",
|
||||||
|
"fieldname": "use_transaction_date_exchange_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Transaction Date Exchange Rate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@ -171,7 +179,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-02 17:02:14.404622",
|
"modified": "2023-10-16 16:22:03.201078",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
@ -584,6 +584,17 @@ class AccountsController(TransactionBase):
|
|||||||
self.currency, self.company_currency, transaction_date, args
|
self.currency, self.company_currency, transaction_date, args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.currency
|
||||||
|
and buying_or_selling == "Buying"
|
||||||
|
and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate")
|
||||||
|
and self.doctype == "Purchase Invoice"
|
||||||
|
):
|
||||||
|
self.use_transaction_date_exchange_rate = True
|
||||||
|
self.conversion_rate = get_exchange_rate(
|
||||||
|
self.currency, self.company_currency, transaction_date, args
|
||||||
|
)
|
||||||
|
|
||||||
def set_missing_item_details(self, for_validate=False):
|
def set_missing_item_details(self, for_validate=False):
|
||||||
"""set missing item values"""
|
"""set missing item values"""
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
@ -25,6 +25,9 @@ class calculate_taxes_and_totals(object):
|
|||||||
def __init__(self, doc: Document):
|
def __init__(self, doc: Document):
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
frappe.flags.round_off_applicable_accounts = []
|
frappe.flags.round_off_applicable_accounts = []
|
||||||
|
frappe.flags.round_row_wise_tax = frappe.db.get_single_value(
|
||||||
|
"Accounts Settings", "round_row_wise_tax"
|
||||||
|
)
|
||||||
|
|
||||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||||
|
|
||||||
@ -370,6 +373,8 @@ class calculate_taxes_and_totals(object):
|
|||||||
for i, tax in enumerate(self.doc.get("taxes")):
|
for i, tax in enumerate(self.doc.get("taxes")):
|
||||||
# tax_amount represents the amount of tax for the current step
|
# tax_amount represents the amount of tax for the current step
|
||||||
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
|
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
|
||||||
|
if frappe.flags.round_row_wise_tax:
|
||||||
|
current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount"))
|
||||||
|
|
||||||
# Adjust divisional loss to the last item
|
# Adjust divisional loss to the last item
|
||||||
if tax.charge_type == "Actual":
|
if tax.charge_type == "Actual":
|
||||||
@ -480,10 +485,19 @@ class calculate_taxes_and_totals(object):
|
|||||||
# store tax breakup for each item
|
# store tax breakup for each item
|
||||||
key = item.item_code or item.item_name
|
key = item.item_code or item.item_name
|
||||||
item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate
|
item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate
|
||||||
if tax.item_wise_tax_detail.get(key):
|
if frappe.flags.round_row_wise_tax:
|
||||||
item_wise_tax_amount += tax.item_wise_tax_detail[key][1]
|
item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount"))
|
||||||
|
if tax.item_wise_tax_detail.get(key):
|
||||||
|
item_wise_tax_amount += flt(tax.item_wise_tax_detail[key][1], tax.precision("tax_amount"))
|
||||||
|
tax.item_wise_tax_detail[key] = [
|
||||||
|
tax_rate,
|
||||||
|
flt(item_wise_tax_amount, tax.precision("tax_amount")),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
if tax.item_wise_tax_detail.get(key):
|
||||||
|
item_wise_tax_amount += tax.item_wise_tax_detail[key][1]
|
||||||
|
|
||||||
tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)]
|
tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)]
|
||||||
|
|
||||||
def round_off_totals(self, tax):
|
def round_off_totals(self, tax):
|
||||||
if tax.account_head in frappe.flags.round_off_applicable_accounts:
|
if tax.account_head in frappe.flags.round_off_applicable_accounts:
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "task.subject",
|
"fetch_from": "task.subject",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -31,7 +32,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "task.project",
|
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
@ -40,7 +40,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-09 11:34:14.335853",
|
"modified": "2023-10-17 12:45:21.536165",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Task Depends On",
|
"name": "Task Depends On",
|
||||||
|
@ -193,7 +193,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
frappe.flags.round_off_applicable_accounts = [];
|
frappe.flags.round_off_applicable_accounts = [];
|
||||||
|
|
||||||
if (me.frm.doc.company) {
|
if (me.frm.doc.company) {
|
||||||
return frappe.call({
|
frappe.call({
|
||||||
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
|
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
|
||||||
"args": {
|
"args": {
|
||||||
"company": me.frm.doc.company,
|
"company": me.frm.doc.company,
|
||||||
@ -206,6 +206,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||||
|
.then((round_row_wise_tax) => {
|
||||||
|
frappe.flags.round_row_wise_tax = round_row_wise_tax;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
determine_exclusive_rate() {
|
determine_exclusive_rate() {
|
||||||
@ -346,6 +351,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||||
// tax_amount represents the amount of tax for the current step
|
// tax_amount represents the amount of tax for the current step
|
||||||
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
|
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
|
||||||
|
if (frappe.flags.round_row_wise_tax) {
|
||||||
|
current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust divisional loss to the last item
|
// Adjust divisional loss to the last item
|
||||||
if (tax.charge_type == "Actual") {
|
if (tax.charge_type == "Actual") {
|
||||||
@ -480,8 +488,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate;
|
let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate;
|
||||||
if (tax_detail && tax_detail[key])
|
if (frappe.flags.round_row_wise_tax) {
|
||||||
item_wise_tax_amount += tax_detail[key][1];
|
item_wise_tax_amount = flt(item_wise_tax_amount, precision("tax_amount", tax));
|
||||||
|
if (tax_detail && tax_detail[key]) {
|
||||||
|
item_wise_tax_amount += flt(tax_detail[key][1], precision("tax_amount", tax));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tax_detail && tax_detail[key])
|
||||||
|
item_wise_tax_amount += tax_detail[key][1];
|
||||||
|
}
|
||||||
|
|
||||||
tax_detail[key] = [tax_rate, flt(item_wise_tax_amount, precision("base_tax_amount", tax))];
|
tax_detail[key] = [tax_rate, flt(item_wise_tax_amount, precision("base_tax_amount", tax))];
|
||||||
}
|
}
|
||||||
|
@ -97,14 +97,14 @@ erpnext.ItemSelector = class ItemSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var me = this;
|
var me = this;
|
||||||
frappe.link_search("Item", args, function(r) {
|
frappe.link_search("Item", args, function(results) {
|
||||||
$.each(r.values, function(i, d) {
|
$.each(results, function(i, d) {
|
||||||
if(!d.image) {
|
if(!d.image) {
|
||||||
d.abbr = frappe.get_abbr(d.item_name);
|
d.abbr = frappe.get_abbr(d.item_name);
|
||||||
d.color = frappe.get_palette(d.item_name);
|
d.color = frappe.get_palette(d.item_name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values}));
|
me.dialog.results.html(frappe.render_template('item_selector', {'data': results}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -7,32 +7,32 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax
|
|||||||
|
|
||||||
|
|
||||||
def update_itemised_tax_data(doc):
|
def update_itemised_tax_data(doc):
|
||||||
|
# maybe this should be a standard function rather than a regional one
|
||||||
if not doc.taxes:
|
if not doc.taxes:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not doc.items:
|
||||||
|
return
|
||||||
|
|
||||||
|
meta = frappe.get_meta(doc.items[0].doctype)
|
||||||
|
if not meta.has_field("tax_rate"):
|
||||||
|
return
|
||||||
|
|
||||||
itemised_tax = get_itemised_tax(doc.taxes)
|
itemised_tax = get_itemised_tax(doc.taxes)
|
||||||
|
|
||||||
for row in doc.items:
|
for row in doc.items:
|
||||||
tax_rate = 0.0
|
tax_rate, tax_amount = 0.0, 0.0
|
||||||
item_tax_rate = 0.0
|
# dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate
|
||||||
|
item_code = row.item_code or row.item_name
|
||||||
|
if itemised_tax.get(item_code):
|
||||||
|
for tax in itemised_tax.get(row.item_code).values():
|
||||||
|
_tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate"))
|
||||||
|
tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
|
||||||
|
tax_rate += _tax_rate
|
||||||
|
|
||||||
if row.item_tax_rate:
|
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
|
||||||
item_tax_rate = frappe.parse_json(row.item_tax_rate)
|
row.tax_amount = flt(tax_amount, row.precision("tax_amount"))
|
||||||
|
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
|
||||||
# First check if tax rate is present
|
|
||||||
# If not then look up in item_wise_tax_detail
|
|
||||||
if item_tax_rate:
|
|
||||||
for account, rate in item_tax_rate.items():
|
|
||||||
tax_rate += rate
|
|
||||||
elif row.item_code and itemised_tax.get(row.item_code):
|
|
||||||
tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()])
|
|
||||||
|
|
||||||
meta = frappe.get_meta(row.doctype)
|
|
||||||
|
|
||||||
if meta.has_field("tax_rate"):
|
|
||||||
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
|
|
||||||
row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
|
|
||||||
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_currency(account):
|
def get_account_currency(account):
|
||||||
|
@ -606,29 +606,37 @@ def close_or_unclose_sales_orders(names, status):
|
|||||||
|
|
||||||
|
|
||||||
def get_requested_item_qty(sales_order):
|
def get_requested_item_qty(sales_order):
|
||||||
return frappe._dict(
|
result = {}
|
||||||
frappe.db.sql(
|
for d in frappe.db.get_all(
|
||||||
"""
|
"Material Request Item",
|
||||||
select sales_order_item, sum(qty)
|
filters={"docstatus": 1, "sales_order": sales_order},
|
||||||
from `tabMaterial Request Item`
|
fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"],
|
||||||
where docstatus = 1
|
group_by="sales_order_item",
|
||||||
and sales_order = %s
|
):
|
||||||
group by sales_order_item
|
result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty})
|
||||||
""",
|
|
||||||
sales_order,
|
return result
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_material_request(source_name, target_doc=None):
|
def make_material_request(source_name, target_doc=None):
|
||||||
requested_item_qty = get_requested_item_qty(source_name)
|
requested_item_qty = get_requested_item_qty(source_name)
|
||||||
|
|
||||||
|
def get_remaining_qty(so_item):
|
||||||
|
return flt(
|
||||||
|
flt(so_item.qty)
|
||||||
|
- flt(requested_item_qty.get(so_item.name, {}).get("qty"))
|
||||||
|
- max(
|
||||||
|
flt(so_item.get("delivered_qty"))
|
||||||
|
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def update_item(source, target, source_parent):
|
def update_item(source, target, source_parent):
|
||||||
# qty is for packed items, because packed items don't have stock_qty field
|
# qty is for packed items, because packed items don't have stock_qty field
|
||||||
qty = source.get("qty")
|
|
||||||
target.project = source_parent.project
|
target.project = source_parent.project
|
||||||
target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
|
target.qty = get_remaining_qty(source)
|
||||||
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
||||||
|
|
||||||
args = target.as_dict().copy()
|
args = target.as_dict().copy()
|
||||||
@ -661,8 +669,8 @@ def make_material_request(source_name, target_doc=None):
|
|||||||
"Sales Order Item": {
|
"Sales Order Item": {
|
||||||
"doctype": "Material Request Item",
|
"doctype": "Material Request Item",
|
||||||
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
||||||
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
|
"condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
|
||||||
and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
|
and get_remaining_qty(item) > 0,
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -157,6 +157,22 @@
|
|||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
@ -166,4 +182,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "full_name",
|
"title_field": "full_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -860,6 +860,22 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
"submit": 0,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
@ -872,4 +888,4 @@
|
|||||||
"title_field": "",
|
"title_field": "",
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 0
|
"track_seen": 0
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ def after_install():
|
|||||||
add_app_name()
|
add_app_name()
|
||||||
setup_log_settings()
|
setup_log_settings()
|
||||||
hide_workspaces()
|
hide_workspaces()
|
||||||
|
update_roles()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -232,6 +233,12 @@ def hide_workspaces():
|
|||||||
frappe.db.set_value("Workspace", ws, "public", 0)
|
frappe.db.set_value("Workspace", ws, "public", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def update_roles():
|
||||||
|
website_user_roles = ("Customer", "Supplier")
|
||||||
|
for role in website_user_roles:
|
||||||
|
frappe.db.set_value("Role", role, "desk_access", 0)
|
||||||
|
|
||||||
|
|
||||||
def create_default_role_profiles():
|
def create_default_role_profiles():
|
||||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||||
role_profile = frappe.new_doc("Role Profile")
|
role_profile = frappe.new_doc("Role Profile")
|
||||||
|
@ -1460,6 +1460,36 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Stock Manager",
|
"role": "Stock Manager",
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "status,customer,customer_name, territory,base_grand_total",
|
"search_fields": "status,customer,customer_name, territory,base_grand_total",
|
||||||
|
@ -725,7 +725,8 @@
|
|||||||
"label": "Against Delivery Note Item",
|
"label": "Against Delivery Note Item",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "stock_qty_sec_break",
|
"fieldname": "stock_qty_sec_break",
|
||||||
@ -892,7 +893,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-26 12:53:49.357171",
|
"modified": "2023-10-16 16:18:18.013379",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
@ -902,4 +903,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
@ -239,7 +239,7 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"report": 0,
|
||||||
"role": "System Manager",
|
"role": "Delivery Manager",
|
||||||
"set_user_permissions": 0,
|
"set_user_permissions": 0,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
"submit": 0,
|
||||||
@ -255,4 +255,4 @@
|
|||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 0,
|
"track_seen": 0,
|
||||||
"track_views": 0
|
"track_views": 0
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-27 11:22:27.927637",
|
"modified": "2023-10-01 07:06:06.314503",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Trip",
|
"name": "Delivery Trip",
|
||||||
@ -224,10 +224,40 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Delivery Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "driver_name"
|
"title_field": "driver_name"
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
|
|||||||
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
|
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
|
||||||
&& frm.doc.__onload.has_stock_ledger.length) {
|
&& frm.doc.__onload.has_stock_ledger.length) {
|
||||||
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
|
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
|
||||||
'type_of_transaction', 'condition', 'mandatory_depends_on'];
|
'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock'];
|
||||||
|
|
||||||
frm.fields.forEach((field) => {
|
frm.fields.forEach((field) => {
|
||||||
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
|
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"target_fieldname",
|
"target_fieldname",
|
||||||
"applicable_for_documents_tab",
|
"applicable_for_documents_tab",
|
||||||
"apply_to_all_doctypes",
|
"apply_to_all_doctypes",
|
||||||
|
"column_break_niy2u",
|
||||||
|
"validate_negative_stock",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"document_type",
|
"document_type",
|
||||||
"type_of_transaction",
|
"type_of_transaction",
|
||||||
@ -173,11 +175,21 @@
|
|||||||
"fieldname": "reqd",
|
"fieldname": "reqd",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Mandatory"
|
"label": "Mandatory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_niy2u",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "validate_negative_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Validate Negative Stock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-31 13:44:38.507698",
|
"modified": "2023-10-05 12:52:18.705431",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Inventory Dimension",
|
"name": "Inventory Dimension",
|
||||||
|
@ -60,6 +60,7 @@ class InventoryDimension(Document):
|
|||||||
"fetch_from_parent",
|
"fetch_from_parent",
|
||||||
"type_of_transaction",
|
"type_of_transaction",
|
||||||
"condition",
|
"condition",
|
||||||
|
"validate_negative_stock",
|
||||||
]
|
]
|
||||||
|
|
||||||
for field in frappe.get_meta("Inventory Dimension").fields:
|
for field in frappe.get_meta("Inventory Dimension").fields:
|
||||||
@ -160,6 +161,7 @@ class InventoryDimension(Document):
|
|||||||
insert_after="inventory_dimension",
|
insert_after="inventory_dimension",
|
||||||
options=self.reference_document,
|
options=self.reference_document,
|
||||||
label=label,
|
label=label,
|
||||||
|
search_index=1,
|
||||||
reqd=self.reqd,
|
reqd=self.reqd,
|
||||||
mandatory_depends_on=self.mandatory_depends_on,
|
mandatory_depends_on=self.mandatory_depends_on,
|
||||||
),
|
),
|
||||||
@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None:
|
|||||||
def get_inventory_documents(
|
def get_inventory_documents(
|
||||||
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
|
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
|
||||||
):
|
):
|
||||||
and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]]
|
and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]]
|
||||||
or_filters = [
|
or_filters = [
|
||||||
["DocField", "options", "in", ["Batch", "Serial No"]],
|
["DocField", "options", "in", ["Batch", "Serial No"]],
|
||||||
["DocField", "parent", "in", ["Putaway Rule"]],
|
["DocField", "parent", "in", ["Putaway Rule"]],
|
||||||
@ -340,6 +342,7 @@ def get_inventory_dimensions():
|
|||||||
fields=[
|
fields=[
|
||||||
"distinct target_fieldname as fieldname",
|
"distinct target_fieldname as fieldname",
|
||||||
"reference_document as doctype",
|
"reference_document as doctype",
|
||||||
|
"validate_negative_stock",
|
||||||
],
|
],
|
||||||
filters={"disabled": 0},
|
filters={"disabled": 0},
|
||||||
)
|
)
|
||||||
|
@ -414,6 +414,53 @@ class TestInventoryDimension(FrappeTestCase):
|
|||||||
else:
|
else:
|
||||||
self.assertEqual(d.store, "Inter Transfer Store 2")
|
self.assertEqual(d.store, "Inter Transfer Store 2")
|
||||||
|
|
||||||
|
def test_validate_negative_stock_for_inventory_dimension(self):
|
||||||
|
frappe.local.inventory_dimensions = {}
|
||||||
|
item_code = "Test Negative Inventory Dimension Item"
|
||||||
|
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
|
||||||
|
create_item(item_code)
|
||||||
|
|
||||||
|
inv_dimension = create_inventory_dimension(
|
||||||
|
apply_to_all_doctypes=1,
|
||||||
|
dimension_name="Inv Site",
|
||||||
|
reference_document="Inv Site",
|
||||||
|
document_type="Inv Site",
|
||||||
|
validate_negative_stock=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse = create_warehouse("Negative Stock Warehouse")
|
||||||
|
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
|
||||||
|
|
||||||
|
doc.items[0].to_inv_site = "Site 1"
|
||||||
|
doc.submit()
|
||||||
|
|
||||||
|
site_name = frappe.get_all(
|
||||||
|
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
||||||
|
)[0].inv_site
|
||||||
|
|
||||||
|
self.assertEqual(site_name, "Site 1")
|
||||||
|
|
||||||
|
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
|
||||||
|
|
||||||
|
doc.items[0].inv_site = "Site 1"
|
||||||
|
self.assertRaises(frappe.ValidationError, doc.submit)
|
||||||
|
|
||||||
|
inv_dimension.reload()
|
||||||
|
inv_dimension.db_set("validate_negative_stock", 0)
|
||||||
|
frappe.local.inventory_dimensions = {}
|
||||||
|
|
||||||
|
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
|
||||||
|
|
||||||
|
doc.items[0].inv_site = "Site 1"
|
||||||
|
doc.submit()
|
||||||
|
self.assertEqual(doc.docstatus, 1)
|
||||||
|
|
||||||
|
site_name = frappe.get_all(
|
||||||
|
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
||||||
|
)[0].inv_site
|
||||||
|
|
||||||
|
self.assertEqual(site_name, "Site 1")
|
||||||
|
|
||||||
|
|
||||||
def get_voucher_sl_entries(voucher_no, fields):
|
def get_voucher_sl_entries(voucher_no, fields):
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
@ -504,6 +551,26 @@ def prepare_test_data():
|
|||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
if not frappe.db.exists("DocType", "Inv Site"):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "DocType",
|
||||||
|
"name": "Inv Site",
|
||||||
|
"module": "Stock",
|
||||||
|
"custom": 1,
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"autoname": "field:site_name",
|
||||||
|
"fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}],
|
||||||
|
"permissions": [
|
||||||
|
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
for site in ["Site 1", "Site 2"]:
|
||||||
|
if not frappe.db.exists("Inv Site", site):
|
||||||
|
frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
def create_inventory_dimension(**args):
|
def create_inventory_dimension(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -366,7 +366,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
stock_value_diff = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
|
stock_value_diff = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
|
||||||
elif flt(item.valuation_rate) and flt(item.qty):
|
elif (flt(item.valuation_rate) or self.is_return) and flt(item.qty):
|
||||||
# If PR is sub-contracted and fg item rate is zero
|
# If PR is sub-contracted and fg item rate is zero
|
||||||
# in that case if account for source and target warehouse are same,
|
# in that case if account for source and target warehouse are same,
|
||||||
# then GL entries should not be posted
|
# then GL entries should not be posted
|
||||||
|
@ -2086,6 +2086,62 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
return_pr.reload()
|
return_pr.reload()
|
||||||
self.assertEqual(return_pr.status, "Completed")
|
self.assertEqual(return_pr.status, "Completed")
|
||||||
|
|
||||||
|
def test_purchase_return_with_zero_rate(self):
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
|
# Step - 1: Create Item
|
||||||
|
item, warehouse = (
|
||||||
|
make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name,
|
||||||
|
"Stores - TCP1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step - 2: Create Stock Entry (Material Receipt)
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
purpose="Material Receipt",
|
||||||
|
item_code=item,
|
||||||
|
qty=100,
|
||||||
|
basic_rate=100,
|
||||||
|
to_warehouse=warehouse,
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step - 3: Create Purchase Receipt
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item,
|
||||||
|
qty=5,
|
||||||
|
rate=0,
|
||||||
|
warehouse=warehouse,
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step - 4: Create Purchase Return
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
|
||||||
|
pr_return = make_return_doc("Purchase Receipt", pr.name)
|
||||||
|
pr_return.save()
|
||||||
|
pr_return.submit()
|
||||||
|
|
||||||
|
sl_entries = get_sl_entries(pr_return.doctype, pr_return.name)
|
||||||
|
gl_entries = get_gl_entries(pr_return.doctype, pr_return.name)
|
||||||
|
|
||||||
|
# Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate
|
||||||
|
average_rate = (
|
||||||
|
(se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate)
|
||||||
|
) / (se.items[0].qty + pr.items[0].qty)
|
||||||
|
expected_stock_value_difference = pr_return.items[0].qty * average_rate
|
||||||
|
self.assertEqual(
|
||||||
|
flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test - 2: GL Entries should be created for Stock Value Difference
|
||||||
|
self.assertEqual(len(gl_entries), 2)
|
||||||
|
|
||||||
|
# Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries.
|
||||||
|
for entry in gl_entries:
|
||||||
|
self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference))
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.core.doctype.role.role import get_users
|
from frappe.core.doctype.role.role import get_users
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
|
from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
||||||
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchBundle
|
from erpnext.stock.serial_batch_bundle import SerialBatchBundle
|
||||||
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
|
|
||||||
class StockFreezeError(frappe.ValidationError):
|
class StockFreezeError(frappe.ValidationError):
|
||||||
@ -48,6 +50,69 @@ class StockLedgerEntry(Document):
|
|||||||
self.validate_and_set_fiscal_year()
|
self.validate_and_set_fiscal_year()
|
||||||
self.block_transactions_against_group_warehouse()
|
self.block_transactions_against_group_warehouse()
|
||||||
self.validate_with_last_transaction_posting_time()
|
self.validate_with_last_transaction_posting_time()
|
||||||
|
self.validate_inventory_dimension_negative_stock()
|
||||||
|
|
||||||
|
def validate_inventory_dimension_negative_stock(self):
|
||||||
|
extra_cond = ""
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
dimensions = self._get_inventory_dimensions()
|
||||||
|
if not dimensions:
|
||||||
|
return
|
||||||
|
|
||||||
|
for dimension, values in dimensions.items():
|
||||||
|
kwargs[dimension] = values.get("value")
|
||||||
|
extra_cond += f" and {dimension} = %({dimension})s"
|
||||||
|
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"item_code": self.item_code,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
"company": self.company,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sle = get_previous_sle(kwargs, extra_cond=extra_cond)
|
||||||
|
if sle:
|
||||||
|
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||||
|
diff = sle.qty_after_transaction + flt(self.actual_qty)
|
||||||
|
diff = flt(diff, flt_precision)
|
||||||
|
if diff < 0 and abs(diff) > 0.0001:
|
||||||
|
self.throw_validation_error(diff, dimensions)
|
||||||
|
|
||||||
|
def throw_validation_error(self, diff, dimensions):
|
||||||
|
dimension_msg = _(", with the inventory {0}: {1}").format(
|
||||||
|
"dimensions" if len(dimensions) > 1 else "dimension",
|
||||||
|
", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = _(
|
||||||
|
"{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction."
|
||||||
|
).format(
|
||||||
|
abs(diff),
|
||||||
|
frappe.get_desk_link("Item", self.item_code),
|
||||||
|
frappe.get_desk_link("Warehouse", self.warehouse),
|
||||||
|
dimension_msg,
|
||||||
|
self.posting_date,
|
||||||
|
self.posting_time,
|
||||||
|
frappe.get_desk_link(self.voucher_type, self.voucher_no),
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.throw(msg, title=_("Inventory Dimension Negative Stock"))
|
||||||
|
|
||||||
|
def _get_inventory_dimensions(self):
|
||||||
|
inv_dimensions = get_inventory_dimensions()
|
||||||
|
inv_dimension_dict = {}
|
||||||
|
for dimension in inv_dimensions:
|
||||||
|
if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname):
|
||||||
|
continue
|
||||||
|
|
||||||
|
dimension["value"] = self.get(dimension.fieldname)
|
||||||
|
inv_dimension_dict.setdefault(dimension.fieldname, dimension)
|
||||||
|
|
||||||
|
return inv_dimension_dict
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.check_stock_frozen_date()
|
self.check_stock_frozen_date()
|
||||||
|
@ -12,6 +12,7 @@ import erpnext
|
|||||||
from erpnext.accounts.utils import get_company_default
|
from erpnext.accounts.utils import get_company_default
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
|
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
|
||||||
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
get_available_serial_nos,
|
get_available_serial_nos,
|
||||||
)
|
)
|
||||||
@ -50,6 +51,7 @@ class StockReconciliation(StockController):
|
|||||||
self.clean_serial_nos()
|
self.clean_serial_nos()
|
||||||
self.set_total_qty_and_amount()
|
self.set_total_qty_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
self.validate_inventory_dimension()
|
||||||
|
|
||||||
if self._action == "submit":
|
if self._action == "submit":
|
||||||
self.validate_reserved_stock()
|
self.validate_reserved_stock()
|
||||||
@ -57,6 +59,17 @@ class StockReconciliation(StockController):
|
|||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.set_serial_and_batch_bundle(ignore_validate=True)
|
self.set_serial_and_batch_bundle(ignore_validate=True)
|
||||||
|
|
||||||
|
def validate_inventory_dimension(self):
|
||||||
|
dimensions = get_inventory_dimensions()
|
||||||
|
for dimension in dimensions:
|
||||||
|
for row in self.items:
|
||||||
|
if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries."
|
||||||
|
).format(row.idx, bold(dimension.get("doctype")))
|
||||||
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
@ -202,8 +215,19 @@ class StockReconciliation(StockController):
|
|||||||
self.calculate_difference_amount(item, bundle_data)
|
self.calculate_difference_amount(item, bundle_data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
inventory_dimensions_dict = {}
|
||||||
|
if not item.batch_no and not item.serial_no:
|
||||||
|
for dimension in get_inventory_dimensions():
|
||||||
|
if item.get(dimension.get("fieldname")):
|
||||||
|
inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname"))
|
||||||
|
|
||||||
item_dict = get_stock_balance_for(
|
item_dict = get_stock_balance_for(
|
||||||
item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no
|
item.item_code,
|
||||||
|
item.warehouse,
|
||||||
|
self.posting_date,
|
||||||
|
self.posting_time,
|
||||||
|
batch_no=item.batch_no,
|
||||||
|
inventory_dimensions_dict=inventory_dimensions_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (item.qty is None or item.qty == item_dict.get("qty")) and (
|
if (item.qty is None or item.qty == item_dict.get("qty")) and (
|
||||||
@ -507,7 +531,13 @@ class StockReconciliation(StockController):
|
|||||||
if not row.batch_no:
|
if not row.batch_no:
|
||||||
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
|
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
|
||||||
|
|
||||||
if self.docstatus == 2:
|
dimensions = get_inventory_dimensions()
|
||||||
|
has_dimensions = False
|
||||||
|
for dimension in dimensions:
|
||||||
|
if row.get(dimension.get("fieldname")):
|
||||||
|
has_dimensions = True
|
||||||
|
|
||||||
|
if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle):
|
||||||
if row.current_qty:
|
if row.current_qty:
|
||||||
data.actual_qty = -1 * row.current_qty
|
data.actual_qty = -1 * row.current_qty
|
||||||
data.qty_after_transaction = flt(row.current_qty)
|
data.qty_after_transaction = flt(row.current_qty)
|
||||||
@ -523,6 +553,13 @@ class StockReconciliation(StockController):
|
|||||||
data.valuation_rate = flt(row.valuation_rate)
|
data.valuation_rate = flt(row.valuation_rate)
|
||||||
data.stock_value_difference = -1 * flt(row.amount_difference)
|
data.stock_value_difference = -1 * flt(row.amount_difference)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle)
|
||||||
|
):
|
||||||
|
data.actual_qty = row.qty
|
||||||
|
data.qty_after_transaction = 0.0
|
||||||
|
data.incoming_rate = flt(row.valuation_rate)
|
||||||
|
|
||||||
self.update_inventory_dimensions(row, data)
|
self.update_inventory_dimensions(row, data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -911,6 +948,7 @@ def get_stock_balance_for(
|
|||||||
posting_time,
|
posting_time,
|
||||||
batch_no: Optional[str] = None,
|
batch_no: Optional[str] = None,
|
||||||
with_valuation_rate: bool = True,
|
with_valuation_rate: bool = True,
|
||||||
|
inventory_dimensions_dict=None,
|
||||||
):
|
):
|
||||||
frappe.has_permission("Stock Reconciliation", "write", throw=True)
|
frappe.has_permission("Stock Reconciliation", "write", throw=True)
|
||||||
|
|
||||||
@ -939,6 +977,7 @@ def get_stock_balance_for(
|
|||||||
posting_time,
|
posting_time,
|
||||||
with_valuation_rate=with_valuation_rate,
|
with_valuation_rate=with_valuation_rate,
|
||||||
with_serial_no=has_serial_no,
|
with_serial_no=has_serial_no,
|
||||||
|
inventory_dimensions_dict=inventory_dimensions_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_serial_no:
|
if has_serial_no:
|
||||||
|
@ -24,6 +24,7 @@ from frappe.utils import (
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||||
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
|
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
|
||||||
)
|
)
|
||||||
@ -711,10 +712,17 @@ class update_entries_after(object):
|
|||||||
):
|
):
|
||||||
sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
|
sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
|
||||||
|
|
||||||
|
dimensions = get_inventory_dimensions()
|
||||||
|
has_dimensions = False
|
||||||
|
if dimensions:
|
||||||
|
for dimension in dimensions:
|
||||||
|
if sle.get(dimension.get("fieldname")):
|
||||||
|
has_dimensions = True
|
||||||
|
|
||||||
if sle.serial_and_batch_bundle:
|
if sle.serial_and_batch_bundle:
|
||||||
self.calculate_valuation_for_serial_batch_bundle(sle)
|
self.calculate_valuation_for_serial_batch_bundle(sle)
|
||||||
else:
|
else:
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
||||||
# assert
|
# assert
|
||||||
self.wh_data.valuation_rate = sle.valuation_rate
|
self.wh_data.valuation_rate = sle.valuation_rate
|
||||||
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||||
@ -1297,7 +1305,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
|||||||
return sle[0] if sle else frappe._dict()
|
return sle[0] if sle else frappe._dict()
|
||||||
|
|
||||||
|
|
||||||
def get_previous_sle(args, for_update=False):
|
def get_previous_sle(args, for_update=False, extra_cond=None):
|
||||||
"""
|
"""
|
||||||
get the last sle on or before the current time-bucket,
|
get the last sle on or before the current time-bucket,
|
||||||
to get actual qty before transaction, this function
|
to get actual qty before transaction, this function
|
||||||
@ -1312,7 +1320,9 @@ def get_previous_sle(args, for_update=False):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
args["name"] = args.get("sle", None) or ""
|
args["name"] = args.get("sle", None) or ""
|
||||||
sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
|
sle = get_stock_ledger_entries(
|
||||||
|
args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
|
||||||
|
)
|
||||||
return sle and sle[0] or {}
|
return sle and sle[0] or {}
|
||||||
|
|
||||||
|
|
||||||
@ -1324,6 +1334,7 @@ def get_stock_ledger_entries(
|
|||||||
for_update=False,
|
for_update=False,
|
||||||
debug=False,
|
debug=False,
|
||||||
check_serial_no=True,
|
check_serial_no=True,
|
||||||
|
extra_cond=None,
|
||||||
):
|
):
|
||||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||||
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
|
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
|
||||||
@ -1361,6 +1372,9 @@ def get_stock_ledger_entries(
|
|||||||
if operator in (">", "<=") and previous_sle.get("name"):
|
if operator in (">", "<=") and previous_sle.get("name"):
|
||||||
conditions += " and name!=%(name)s"
|
conditions += " and name!=%(name)s"
|
||||||
|
|
||||||
|
if extra_cond:
|
||||||
|
conditions += f"{extra_cond}"
|
||||||
|
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select *, timestamp(posting_date, posting_time) as "timestamp"
|
select *, timestamp(posting_date, posting_time) as "timestamp"
|
||||||
|
@ -95,6 +95,7 @@ def get_stock_balance(
|
|||||||
posting_time=None,
|
posting_time=None,
|
||||||
with_valuation_rate=False,
|
with_valuation_rate=False,
|
||||||
with_serial_no=False,
|
with_serial_no=False,
|
||||||
|
inventory_dimensions_dict=None,
|
||||||
):
|
):
|
||||||
"""Returns stock balance quantity at given warehouse on given posting date or current date.
|
"""Returns stock balance quantity at given warehouse on given posting date or current date.
|
||||||
|
|
||||||
@ -114,7 +115,13 @@ def get_stock_balance(
|
|||||||
"posting_time": posting_time,
|
"posting_time": posting_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
last_entry = get_previous_sle(args)
|
extra_cond = ""
|
||||||
|
if inventory_dimensions_dict:
|
||||||
|
for field, value in inventory_dimensions_dict.items():
|
||||||
|
args[field] = value
|
||||||
|
extra_cond += f" and {field} = %({field})s"
|
||||||
|
|
||||||
|
last_entry = get_previous_sle(args, extra_cond=extra_cond)
|
||||||
|
|
||||||
if with_valuation_rate:
|
if with_valuation_rate:
|
||||||
if with_serial_no:
|
if with_serial_no:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user