Merge branch 'version-14-hotfix' of https://github.com/frappe/erpnext into default_dates_in_reports

This commit is contained in:
Deepesh Garg
2024-04-24 17:20:55 +05:30
1375 changed files with 42896 additions and 28888 deletions

View File

@@ -17,9 +17,9 @@ install_docs = [
def get_warehouse_account_map(company=None):
company_warehouse_account_map = company and frappe.flags.setdefault(
"warehouse_account_map", {}
).get(company)
company_warehouse_account_map = company and frappe.flags.setdefault("warehouse_account_map", {}).get(
company
)
warehouse_account_map = frappe.flags.warehouse_account_map
if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test:

View File

@@ -1,4 +1,4 @@
frappe.provide('erpnext.stock');
frappe.provide("erpnext.stock");
erpnext.stock.ItemDashboard = class ItemDashboard {
constructor(opts) {
@@ -9,46 +9,51 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
var me = this;
this.start = 0;
if (!this.sort_by) {
this.sort_by = 'projected_qty';
this.sort_order = 'asc';
this.sort_by = "projected_qty";
this.sort_order = "asc";
}
this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent);
this.result = this.content.find('.result');
this.content = $(frappe.render_template("item_dashboard")).appendTo(this.parent);
this.result = this.content.find(".result");
this.content.on('click', '.btn-move', function () {
this.content.on("click", ".btn-move", function () {
handle_move_add($(this), "Move");
});
this.content.on('click', '.btn-add', function () {
this.content.on("click", ".btn-add", function () {
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);
});
this.content.on("click", ".btn-edit", function () {
let item = unescape($(this).attr("data-item"));
let warehouse = unescape($(this).attr("data-warehouse"));
let company = unescape($(this).attr("data-company"));
frappe.db.get_value(
"Putaway Rule",
{
item_code: item,
warehouse: warehouse,
company: company,
},
"name",
(r) => {
frappe.set_route("Form", "Putaway Rule", r.name);
}
);
});
function handle_move_add(element, action) {
let item = unescape(element.attr('data-item'));
let warehouse = unescape(element.attr('data-warehouse'));
let actual_qty = unescape(element.attr('data-actual_qty'));
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry')));
let item = unescape(element.attr("data-item"));
let warehouse = unescape(element.attr("data-warehouse"));
let actual_qty = unescape(element.attr("data-actual_qty"));
let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry")));
let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt";
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr('data-rate'));
let rate = unescape($(this).attr("data-rate"));
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
me.refresh();
});
@@ -61,35 +66,33 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
function open_stock_entry(item, warehouse, entry_type) {
frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry');
frappe.model.with_doctype("Stock Entry", function () {
var doc = frappe.model.get_new_doc("Stock Entry");
if (entry_type) {
doc.stock_entry_type = entry_type;
}
var row = frappe.model.add_child(doc, 'items');
var row = frappe.model.add_child(doc, "items");
row.item_code = item;
if (entry_type === "Material Transfer") {
row.s_warehouse = warehouse;
}
else {
} else {
row.t_warehouse = warehouse;
}
frappe.set_route('Form', doc.doctype, doc.name);
frappe.set_route("Form", doc.doctype, doc.name);
});
}
// more
this.content.find('.btn-more').on('click', function () {
this.content.find(".btn-more").on("click", function () {
me.start += me.page_length;
me.refresh();
});
}
refresh() {
if(this.before_refresh) {
if (this.before_refresh) {
this.before_refresh();
}
@@ -101,7 +104,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
company: this.company,
start: this.start,
sort_by: this.sort_by,
sort_order: this.sort_order
sort_order: this.sort_order,
};
var me = this;
@@ -110,11 +113,11 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
args: args,
callback: function (r) {
me.render(r.message);
}
},
});
}
render(data) {
if (this.start===0) {
if (this.start === 0) {
this.max_count = 0;
this.result.empty();
}
@@ -129,22 +132,22 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
this.max_count = this.max_count;
// show more button
if (data && data.length === (this.page_length + 1)) {
this.content.find('.more').removeClass('hidden');
if (data && data.length === this.page_length + 1) {
this.content.find(".more").removeClass("hidden");
// remove the last element
data.splice(-1);
} else {
this.content.find('.more').addClass('hidden');
this.content.find(".more").addClass("hidden");
}
// If not any stock in any warehouses provide a message to end user
if (context.data.length > 0) {
this.content.find('.result').css('text-align', 'unset');
this.content.find(".result").css("text-align", "unset");
$(frappe.render_template(this.template, context)).appendTo(this.result);
} else {
var message = __("No Stock Available Currently");
this.content.find('.result').css('text-align', 'center');
this.content.find(".result").css("text-align", "center");
$(`<div class='text-muted' style='margin: 20px 5px;'>
${message} </div>`).appendTo(this.result);
@@ -152,19 +155,23 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
get_item_dashboard_data(data, max_count, show_item) {
if(!max_count) max_count = 0;
if(!data) data = [];
if (!max_count) max_count = 0;
if (!data) data = [];
data.forEach(function (d) {
d.actual_or_pending = d.projected_qty + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
d.actual_or_pending =
d.projected_qty +
d.reserved_qty +
d.reserved_qty_for_production +
d.reserved_qty_for_sub_contract;
d.pending_qty = 0;
d.total_reserved = d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
d.total_reserved =
d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
if (d.actual_or_pending > d.actual_qty) {
d.pending_qty = d.actual_or_pending - d.actual_qty;
}
max_count = Math.max(d.actual_or_pending, d.actual_qty,
d.total_reserved, max_count);
max_count = Math.max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count);
});
let can_write = 0;
@@ -176,7 +183,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
data: data,
max_count: max_count,
can_write: can_write,
show_item: show_item || false
show_item: show_item || false,
};
}
@@ -201,73 +208,74 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({
title: target ? __('Add Item') : __('Move Item'),
fields: [{
fieldname: 'item_code',
label: __('Item'),
fieldtype: 'Link',
options: 'Item',
read_only: 1
},
{
fieldname: 'source',
label: __('Source Warehouse'),
fieldtype: 'Link',
options: 'Warehouse',
read_only: 1
},
{
fieldname: 'target',
label: __('Target Warehouse'),
fieldtype: 'Link',
options: 'Warehouse',
reqd: 1,
get_query() {
return {
filters: {
is_group: 0
}
}
}
},
{
fieldname: 'qty',
label: __('Quantity'),
reqd: 1,
fieldtype: 'Float',
description: __('Available {0}', [actual_qty])
},
{
fieldname: 'rate',
label: __('Rate'),
fieldtype: 'Currency',
hidden: 1
},
title: target ? __("Add Item") : __("Move Item"),
fields: [
{
fieldname: "item_code",
label: __("Item"),
fieldtype: "Link",
options: "Item",
read_only: 1,
},
{
fieldname: "source",
label: __("Source Warehouse"),
fieldtype: "Link",
options: "Warehouse",
read_only: 1,
},
{
fieldname: "target",
label: __("Target Warehouse"),
fieldtype: "Link",
options: "Warehouse",
reqd: 1,
get_query() {
return {
filters: {
is_group: 0,
},
};
},
},
{
fieldname: "qty",
label: __("Quantity"),
reqd: 1,
fieldtype: "Float",
description: __("Available {0}", [actual_qty]),
},
{
fieldname: "rate",
label: __("Rate"),
fieldtype: "Currency",
hidden: 1,
},
],
});
dialog.show();
dialog.get_field('item_code').set_input(item);
dialog.get_field("item_code").set_input(item);
if (source) {
dialog.get_field('source').set_input(source);
dialog.get_field("source").set_input(source);
} else {
dialog.get_field('source').df.hidden = 1;
dialog.get_field('source').refresh();
dialog.get_field("source").df.hidden = 1;
dialog.get_field("source").refresh();
}
if (rate) {
dialog.get_field('rate').set_value(rate);
dialog.get_field('rate').df.hidden = 0;
dialog.get_field('rate').refresh();
dialog.get_field("rate").set_value(rate);
dialog.get_field("rate").df.hidden = 0;
dialog.get_field("rate").refresh();
}
if (target) {
dialog.get_field('target').df.read_only = 1;
dialog.get_field('target').value = target;
dialog.get_field('target').refresh();
dialog.get_field("target").df.read_only = 1;
dialog.get_field("target").value = target;
dialog.get_field("target").refresh();
}
dialog.set_primary_action(__('Create Stock Entry'), function () {
dialog.set_primary_action(__("Create Stock Entry"), function () {
if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) {
frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty]));
return;
@@ -278,20 +286,20 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
return;
}
frappe.model.with_doctype('Stock Entry', function () {
let doc = frappe.model.get_new_doc('Stock Entry');
doc.from_warehouse = dialog.get_value('source');
doc.to_warehouse = dialog.get_value('target');
frappe.model.with_doctype("Stock Entry", function () {
let doc = frappe.model.get_new_doc("Stock Entry");
doc.from_warehouse = dialog.get_value("source");
doc.to_warehouse = dialog.get_value("target");
doc.stock_entry_type = doc.from_warehouse ? "Material Transfer" : "Material Receipt";
let row = frappe.model.add_child(doc, 'items');
row.item_code = dialog.get_value('item_code');
row.s_warehouse = dialog.get_value('source');
row.t_warehouse = dialog.get_value('target');
row.qty = dialog.get_value('qty');
let row = frappe.model.add_child(doc, "items");
row.item_code = dialog.get_value("item_code");
row.s_warehouse = dialog.get_value("source");
row.t_warehouse = dialog.get_value("target");
row.qty = dialog.get_value("qty");
row.conversion_factor = 1;
row.transfer_qty = dialog.get_value('qty');
row.basic_rate = dialog.get_value('rate');
frappe.set_route('Form', doc.doctype, doc.name);
row.transfer_qty = dialog.get_value("qty");
row.basic_rate = dialog.get_value("rate");
frappe.set_route("Form", doc.doctype, doc.name);
});
});
};

View File

@@ -1,4 +1,4 @@
frappe.provide('frappe.dashboards.chart_sources');
frappe.provide("frappe.dashboards.chart_sources");
frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = {
method: "erpnext.stock.dashboard_chart_source.warehouse_wise_stock_value.warehouse_wise_stock_value.get",
@@ -8,7 +8,7 @@ frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = {
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company")
}
]
default: frappe.defaults.get_user_default("Company"),
},
],
};

View File

@@ -27,9 +27,7 @@ def get(
if filters and filters.get("company"):
warehouse_filters.append(["company", "=", filters.get("company")])
warehouses = frappe.get_list(
"Warehouse", pluck="name", filters=warehouse_filters, order_by="name"
)
warehouses = frappe.get_list("Warehouse", pluck="name", filters=warehouse_filters, order_by="name")
warehouses = frappe.get_list(
"Bin",

View File

@@ -1,62 +1,72 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on('Batch', {
frappe.ui.form.on("Batch", {
setup: (frm) => {
frm.fields_dict['item'].get_query = function(doc, cdt, cdn) {
frm.fields_dict["item"].get_query = function (doc, cdt, cdn) {
return {
query: "erpnext.controllers.queries.item_query",
filters:{
'is_stock_item': 1,
'has_batch_no': 1
}
}
}
filters: {
is_stock_item: 1,
has_batch_no: 1,
},
};
};
},
refresh: (frm) => {
if(!frm.is_new()) {
if (!frm.is_new()) {
frm.add_custom_button(__("View Ledger"), () => {
frappe.route_options = {
batch_no: frm.doc.name
batch_no: frm.doc.name,
};
frappe.set_route("query-report", "Stock Ledger");
});
frm.trigger('make_dashboard');
frm.trigger("make_dashboard");
}
},
item: (frm) => {
// frappe.db.get_value('Item', {name: frm.doc.item}, 'has_expiry_date', (r) => {
// frm.toggle_reqd('expiry_date', r.has_expiry_date);
// });
frappe.db.get_value('Item', {name: frm.doc.item}, ['shelf_life_in_days', 'has_expiry_date'], (r) => {
if (r.has_expiry_date && r.shelf_life_in_days) {
// Calculate expiry date based on shelf_life_in_days
frm.set_value('expiry_date', frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days));
}else if(r.has_expiry_date){
frm.toggle_reqd('expiry_date', r.has_expiry_date);
frappe.db.get_value(
"Item",
{ name: frm.doc.item },
["shelf_life_in_days", "has_expiry_date"],
(r) => {
if (r.has_expiry_date && r.shelf_life_in_days) {
// Calculate expiry date based on shelf_life_in_days
frm.set_value(
"expiry_date",
frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days)
);
} else if (r.has_expiry_date) {
frm.toggle_reqd("expiry_date", r.has_expiry_date);
}
}
})
);
},
make_dashboard: (frm) => {
if(!frm.is_new()) {
if (!frm.is_new()) {
frappe.call({
method: 'erpnext.stock.doctype.batch.batch.get_batch_qty',
args: {batch_no: frm.doc.name},
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: { batch_no: frm.doc.name },
callback: (r) => {
if(!r.message) {
if (!r.message) {
return;
}
const section = frm.dashboard.add_section('', __("Stock Levels"));
const section = frm.dashboard.add_section("", __("Stock Levels"));
// sort by qty
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
r.message.sort(function (a, b) {
a.qty > b.qty ? 1 : -1;
});
var rows = $('<div></div>').appendTo(section);
const rows = $("<div></div>").appendTo(section);
// show
(r.message || []).forEach(function(d) {
if(d.qty > 0) {
(r.message || []).forEach(function (d) {
if (d.qty > 0) {
$(`<div class='row' style='margin-bottom: 10px;'>
<div class='col-sm-3 small' style='padding-top: 3px;'>${d.warehouse}</div>
<div class='col-sm-3 small text-right' style='padding-top: 3px;'>${d.qty}</div>
@@ -64,101 +74,110 @@ frappe.ui.form.on('Batch', {
<button class='btn btn-default btn-xs btn-move' style='margin-right: 7px;'
data-qty = "${d.qty}"
data-warehouse = "${d.warehouse}">
${__('Move')}</button>
${__("Move")}</button>
<button class='btn btn-default btn-xs btn-split'
data-qty = "${d.qty}"
data-warehouse = "${d.warehouse}">
${__('Split')}</button>
${__("Split")}</button>
</div>
</div>`).appendTo(rows);
}
});
// move - ask for target warehouse and make stock entry
rows.find('.btn-move').on('click', function() {
var $btn = $(this);
rows.find(".btn-move").on("click", function () {
const $btn = $(this);
const fields = [
{
fieldname: 'to_warehouse',
label: __('To Warehouse'),
fieldtype: 'Link',
options: 'Warehouse'
}
fieldname: "to_warehouse",
label: __("To Warehouse"),
fieldtype: "Link",
options: "Warehouse",
},
];
frappe.prompt(
fields,
(data) => {
frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
method: "erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry",
args: {
item_code: frm.doc.item,
batch_no: frm.doc.name,
qty: $btn.attr('data-qty'),
from_warehouse: $btn.attr('data-warehouse'),
qty: $btn.attr("data-qty"),
from_warehouse: $btn.attr("data-warehouse"),
to_warehouse: data.to_warehouse,
source_document: frm.doc.reference_name,
reference_doctype: frm.doc.reference_doctype
reference_doctype: frm.doc.reference_doctype,
},
callback: (r) => {
frappe.show_alert(__('Stock Entry {0} created',
['<a href="/app/stock-entry/'+r.message.name+'">' + r.message.name+ '</a>']));
frappe.show_alert(
__("Stock Entry {0} created", [
'<a href="/app/stock-entry/' +
r.message.name +
'">' +
r.message.name +
"</a>",
])
);
frm.refresh();
},
});
},
__('Select Target Warehouse'),
__('Move')
__("Select Target Warehouse"),
__("Move")
);
});
// split - ask for new qty and batch ID (optional)
// and make stock entry via batch.batch_split
rows.find('.btn-split').on('click', function() {
var $btn = $(this);
frappe.prompt([{
fieldname: 'qty',
label: __('New Batch Qty'),
fieldtype: 'Float',
'default': $btn.attr('data-qty')
},
{
fieldname: 'new_batch_id',
label: __('New Batch ID (Optional)'),
fieldtype: 'Data',
}],
(data) => {
frappe.call({
method: 'erpnext.stock.doctype.batch.batch.split_batch',
args: {
item_code: frm.doc.item,
batch_no: frm.doc.name,
qty: data.qty,
warehouse: $btn.attr('data-warehouse'),
new_batch_id: data.new_batch_id
rows.find(".btn-split").on("click", function () {
const $btn = $(this);
frappe.prompt(
[
{
fieldname: "qty",
label: __("New Batch Qty"),
fieldtype: "Float",
default: $btn.attr("data-qty"),
},
callback: (r) => {
frm.refresh();
{
fieldname: "new_batch_id",
label: __("New Batch ID (Optional)"),
fieldtype: "Data",
},
});
},
__('Split Batch'),
__('Split')
],
(data) => {
frappe
.xcall("erpnext.stock.doctype.batch.batch.split_batch", {
item_code: frm.doc.item,
batch_no: frm.doc.name,
qty: data.qty,
warehouse: $btn.attr("data-warehouse"),
new_batch_id: data.new_batch_id,
})
.then(() => frm.reload_doc());
},
__("Split Batch"),
__("Split")
);
})
});
frm.dashboard.show();
}
},
});
}
}
})
},
});
frappe.ui.form.on('Batch', 'manufacturing_date', function (frm){
frappe.db.get_value('Item', {name: frm.doc.item}, ['shelf_life_in_days', 'has_expiry_date'], (r) => {
frappe.ui.form.on("Batch", "manufacturing_date", function (frm) {
frappe.db.get_value("Item", { name: frm.doc.item }, ["shelf_life_in_days", "has_expiry_date"], (r) => {
if (r.has_expiry_date && r.shelf_life_in_days) {
// Calculate expiry date based on shelf_life_in_days
frm.set_value('expiry_date', frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days));
frm.set_value(
"expiry_date",
frappe.datetime.add_days(frm.doc.manufacturing_date, r.shelf_life_in_days)
);
}
})
})
});
});

View File

@@ -165,9 +165,7 @@ class Batch(Document):
@frappe.whitelist()
def get_batch_qty(
batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None
):
def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -221,9 +219,7 @@ def get_batch_qty(
def get_batches_by_oldest(item_code, warehouse):
"""Returns the oldest batch and qty for the given item_code and warehouse"""
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
batches_dates = [
[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches
]
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
batches_dates.sort(key=lambda tup: tup[1])
return batches_dates

View File

@@ -1,14 +1,21 @@
frappe.listview_settings['Batch'] = {
frappe.listview_settings["Batch"] = {
add_fields: ["item", "expiry_date", "batch_qty", "disabled"],
get_indicator: (doc) => {
if (doc.disabled) {
return [__("Disabled"), "gray", "disabled,=,1"];
} else if (!doc.batch_qty) {
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
} else if (doc.expiry_date && frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0) {
return [__("Expired"), "red", "expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0"]
} else if (
doc.expiry_date &&
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0
) {
return [
__("Expired"),
"red",
"expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0",
];
} else {
return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"];
};
}
}
},
};

View File

@@ -81,9 +81,7 @@ class TestBatch(FrappeTestCase):
stock_entry.submit()
self.assertTrue(stock_entry.items[0].batch_no)
self.assertEqual(
get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90
)
self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90)
def test_delivery_note(self):
"""Test automatic batch selection for outgoing items"""
@@ -159,9 +157,7 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt()
from erpnext.stock.doctype.batch.batch import split_batch
new_batch = split_batch(
receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22
)
new_batch = split_batch(receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22)
self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78)
self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22)
@@ -359,9 +355,7 @@ class TestBatch(FrappeTestCase):
self.make_batch_item(item_code)
def assertValuation(expected):
actual = get_valuation_rate(
item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no
)
actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
self.assertAlmostEqual(actual, expected)
se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bin', {
refresh: function(frm) {
}
frappe.ui.form.on("Bin", {
refresh: function (frm) {},
});

View File

@@ -51,8 +51,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"default": "0.00",

View File

@@ -58,9 +58,7 @@ class Bin(Document):
get_reserved_qty_for_sub_assembly,
)
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
self.item_code, self.warehouse
)
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(self.item_code, self.warehouse)
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
return
@@ -81,9 +79,7 @@ class Bin(Document):
in open work orders"""
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
self.reserved_qty_for_production = get_reserved_qty_for_production(
self.item_code, self.warehouse
)
self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse)
self.db_set(
"reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True
@@ -131,9 +127,7 @@ class Bin(Document):
se_item = frappe.qb.DocType("Stock Entry Detail")
if frappe.db.field_exists("Stock Entry", "is_return"):
qty_field = (
Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
)
qty_field = Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
else:
qty_field = se_item.transfer_qty

View File

@@ -31,4 +31,4 @@ class TestBin(FrappeTestCase):
def test_index_exists(self):
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
self.fail(f"Expected unique index on item-warehouse")
self.fail("Expected unique index on item-warehouse")

View File

@@ -16,9 +16,9 @@ frappe.ui.form.on("Closing Stock Balance", {
freeze: true,
callback: () => {
frm.reload_doc();
}
})
})
},
});
});
}
},
@@ -31,9 +31,9 @@ frappe.ui.form.on("Closing Stock Balance", {
freeze: true,
callback: () => {
frm.reload_doc();
}
})
})
},
});
});
}
}
},
});

View File

@@ -44,7 +44,7 @@ class ClosingStockBalance(Document):
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
| (table.from_date >= self.from_date and table.to_date <= self.to_date)
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
)
)
)
@@ -126,8 +126,6 @@ def prepare_closing_stock_balance(name):
try:
doc.create_closing_stock_balance_entries()
doc.db_set("status", "Completed")
except Exception as e:
except Exception:
doc.db_set("status", "Failed")
traceback = frappe.get_traceback()
frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name)
doc.log_error(title="Closing Stock Balance Failed")

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Customs Tariff Number', {
refresh: function(frm) {
}
frappe.ui.form.on("Customs Tariff Number", {
refresh: function (frm) {},
});

View File

@@ -680,7 +680,7 @@
},
{
"fieldname": "other_charges_calculation",
"fieldtype": "Long Text",
"fieldtype": "Text Editor",
"label": "Taxes and Charges Calculation",
"no_copy": 1,
"oldfieldtype": "HTML",
@@ -1401,7 +1401,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2023-12-18 17:19:39.368239",
"modified": "2024-03-20 16:05:02.854990",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -10,7 +10,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
@@ -20,7 +20,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
class DeliveryNote(SellingController):
def __init__(self, *args, **kwargs):
super(DeliveryNote, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.status_updater = [
{
"source_dt": "Delivery Note Item",
@@ -108,7 +108,7 @@ class DeliveryNote(SellingController):
for f in fieldname:
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
super(DeliveryNote, self).before_print(settings)
super().before_print(settings)
def set_actual_qty(self):
for d in self.get("items"):
@@ -129,7 +129,8 @@ class DeliveryNote(SellingController):
def validate(self):
self.validate_posting_time()
super(DeliveryNote, self).validate()
super().validate()
self.validate_references()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -158,11 +159,16 @@ class DeliveryNote(SellingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(DeliveryNote, self).validate_with_previous_doc(
super().validate_with_previous_doc(
{
"Sales Order": {
"ref_dn_field": "against_sales_order",
"compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
"compare_fields": [
["customer", "="],
["company", "="],
["project", "="],
["currency", "="],
],
},
"Sales Order Item": {
"ref_dn_field": "so_detail",
@@ -172,7 +178,12 @@ class DeliveryNote(SellingController):
},
"Sales Invoice": {
"ref_dn_field": "against_sales_invoice",
"compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
"compare_fields": [
["customer", "="],
["company", "="],
["project", "="],
["currency", "="],
],
},
"Sales Invoice Item": {
"ref_dn_field": "si_detail",
@@ -195,6 +206,58 @@ class DeliveryNote(SellingController):
]
)
def validate_references(self):
self.validate_sales_order_references()
self.validate_sales_invoice_references()
def validate_sales_order_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_order and not item.so_detail) or (
not item.against_sales_order and item.so_detail
):
if not item.against_sales_order:
err_msg += (
_("'Sales Order' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_order")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("so_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
def validate_sales_invoice_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_invoice and not item.si_detail) or (
not item.against_sales_invoice and item.si_detail
):
if not item.against_sales_invoice:
err_msg += (
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("si_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
@@ -210,7 +273,7 @@ class DeliveryNote(SellingController):
)
def validate_warehouse(self):
super(DeliveryNote, self).validate_warehouse()
super().validate_warehouse()
for d in self.get_item_list():
if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1:
@@ -258,7 +321,7 @@ class DeliveryNote(SellingController):
self.repost_future_sle_and_gle()
def on_cancel(self):
super(DeliveryNote, self).on_cancel()
super().on_cancel()
self.check_sales_order_on_hold_or_close("against_sales_order")
self.check_next_docstatus()
@@ -570,7 +633,7 @@ def get_returned_qty_map(delivery_note):
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
def make_sales_invoice(source_name, target_doc=None, args=None):
doc = frappe.get_doc("Delivery Note", source_name)
to_make_invoice_qty_map = {}
@@ -584,6 +647,9 @@ def make_sales_invoice(source_name, target_doc=None):
if len(target.get("items")) == 0:
frappe.throw(_("All these items have already been Invoiced/Returned"))
if args and args.get("merge_taxes"):
merge_taxes(source.get("taxes") or [], target)
target.run_method("calculate_taxes_and_totals")
# set company address
@@ -648,7 +714,11 @@ def make_sales_invoice(source_name, target_doc=None):
if not doc.get("is_return")
else get_pending_qty(d) > 0,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"add_if_empty": True,
"ignore": args.get("merge_taxes") if args else 0,
},
"Sales Team": {
"doctype": "Sales Team",
"field_map": {"incentives": "incentives"},
@@ -702,7 +772,7 @@ def make_delivery_trip(source_name, target_doc=None):
@frappe.whitelist()
def make_installation_note(source_name, target_doc=None):
def make_installation_note(source_name, target_doc=None, kwargs=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.installed_qty)
target.serial_no = obj.serial_no
@@ -791,7 +861,7 @@ def make_shipment(source_name, target_doc=None):
"User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1
)
target.pickup_contact_email = user.email
pickup_contact_display = "{}".format(user.full_name)
pickup_contact_display = f"{user.full_name}"
if user:
if user.email:
pickup_contact_display += "<br>" + user.email
@@ -807,7 +877,7 @@ def make_shipment(source_name, target_doc=None):
contact = frappe.db.get_value(
"Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
)
delivery_contact_display = "{}".format(source.contact_display)
delivery_contact_display = f"{source.contact_display}"
if contact:
if contact.email_id:
delivery_contact_display += "<br>" + contact.email_id
@@ -912,6 +982,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")):
target.append("taxes", tax)
if not target.get("items"):
frappe.throw(_("All items have already been received"))
def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype == "Purchase Receipt":
@@ -967,6 +1040,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
shipping_address_name=target_doc.shipping_address_name,
)
def update_item(source, target, source_parent):
if source_parent.doctype == "Delivery Note" and source.received_qty:
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
doclist = get_mapped_doc(
doctype,
source_name,
@@ -976,8 +1053,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"postprocess": update_details,
"field_no_map": ["taxes_and_charges", "set_warehouse"],
},
doctype
+ " Item": {
doctype + " Item": {
"doctype": target_doctype + " Item",
"field_map": {
source_document_warehouse_field: target_document_warehouse_field,
@@ -990,6 +1066,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"Material_request_item": "material_request_item",
},
"field_no_map": ["warehouse"],
"condition": lambda item: item.received_qty < item.qty + item.returned_qty,
"postprocess": update_item,
},
},
target_doc,

View File

@@ -8,6 +8,7 @@ def get_data():
"Stock Entry": "delivery_note_no",
"Quality Inspection": "reference_name",
"Auto Repeat": "reference_document",
"Purchase Receipt": "inter_company_reference",
},
"internal_links": {
"Sales Order": ["items", "against_sales_order"],
@@ -22,6 +23,9 @@ def get_data():
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
{"label": _("Returns"), "items": ["Stock Entry"]},
{"label": _("Subscription"), "items": ["Auto Repeat"]},
{"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]},
{
"label": _("Internal Transfer"),
"items": ["Material Request", "Purchase Order", "Purchase Receipt"],
},
],
}

View File

@@ -1,8 +1,18 @@
frappe.listview_settings['Delivery Note'] = {
add_fields: ["customer", "customer_name", "base_grand_total", "per_installed", "per_billed",
"transporter_name", "grand_total", "is_return", "status", "currency"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
frappe.listview_settings["Delivery Note"] = {
add_fields: [
"customer",
"customer_name",
"base_grand_total",
"per_installed",
"per_billed",
"transporter_name",
"grand_total",
"is_return",
"status",
"currency",
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1) {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
@@ -24,46 +34,45 @@ frappe.listview_settings['Delivery Note'] = {
if (!doc.docstatus) {
frappe.throw(__("Cannot create a Delivery Trip from Draft documents."));
}
};
}
frappe.new_doc("Delivery Trip")
.then(() => {
// Empty out the child table before inserting new ones
cur_frm.set_value("delivery_stops", []);
frappe.new_doc("Delivery Trip").then(() => {
// Empty out the child table before inserting new ones
cur_frm.set_value("delivery_stops", []);
// We don't want to use `map_current_doc` since it brings up
// the dialog to select more items. We just want the mapper
// function to be called.
frappe.call({
type: "POST",
method: "frappe.model.mapper.map_docs",
args: {
"method": "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
"source_names": docnames,
"target_doc": cur_frm.doc
},
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
cur_frm.dirty();
cur_frm.refresh();
}
// We don't want to use `map_current_doc` since it brings up
// the dialog to select more items. We just want the mapper
// function to be called.
frappe.call({
type: "POST",
method: "frappe.model.mapper.map_docs",
args: {
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
source_names: docnames,
target_doc: cur_frm.doc,
},
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
cur_frm.dirty();
cur_frm.refresh();
}
});
})
};
},
});
});
}
};
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
doclist.page.add_action_item(__('Create Delivery Trip'), action);
doclist.page.add_action_item(__("Create Delivery Trip"), action);
doclist.page.add_action_item(__("Sales Invoice"), ()=>{
doclist.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
});
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
});
}
},
};

View File

@@ -1,15 +1,27 @@
import click
import frappe
UNUSED_INDEXES = [
("Delivery Note", ["customer", "is_return", "return_against"]),
("Sales Invoice", ["customer", "is_return", "return_against"]),
("Purchase Invoice", ["supplier", "is_return", "return_against"]),
("Purchase Receipt", ["supplier", "is_return", "return_against"]),
]
def execute():
"""Drop unused return_against index"""
for doctype, index_fields in UNUSED_INDEXES:
table = f"tab{doctype}"
index_name = frappe.db.get_index_name(index_fields)
drop_index_if_exists(table, index_name)
def drop_index_if_exists(table: str, index: str):
if not frappe.db.has_index(table, index):
return
try:
frappe.db.sql_ddl(
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
)
frappe.db.sql_ddl(
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
)
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
click.echo(f"✓ dropped {index} index from {table}")
except Exception:
frappe.log_error("Failed to drop unused index")
frappe.log_error("Failed to drop index")

View File

@@ -54,7 +54,7 @@ class TestDeliveryNote(FrappeTestCase):
self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert)
def test_delivery_note_no_gl_entry(self):
company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company")
frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company")
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
stock_queue = json.loads(
@@ -71,16 +71,14 @@ class TestDeliveryNote(FrappeTestCase):
dn = create_delivery_note()
sle = frappe.get_doc(
"Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}
)
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2))
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
def test_delivery_note_gl_entry_packing_item(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100)
make_stock_entry(
@@ -127,7 +125,7 @@ class TestDeliveryNote(FrappeTestCase):
stock_in_hand_account: [0.0, stock_value_diff],
"Cost of Goods Sold - TCP1": [stock_value_diff, 0.0],
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
# check stock in hand balance
@@ -159,9 +157,7 @@ class TestDeliveryNote(FrappeTestCase):
serial_no = get_serial_nos(se.get("items")[0].serial_no)
serial_no = "\n".join(serial_no)
dn = create_delivery_note(
item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no
)
dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no)
si = make_sales_invoice(dn.name)
si.items[0].qty = 1
@@ -694,7 +690,7 @@ class TestDeliveryNote(FrappeTestCase):
"Stock In Hand - TCP1": [0.0, stock_value_difference],
target_warehouse: [stock_value_difference, 0.0],
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
# tear down
@@ -723,6 +719,15 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
def test_sales_order_reference_validation(self):
so = make_sales_order(po_no="12345")
dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
dn.items[0].against_sales_order = None
self.assertRaises(frappe.ValidationError, dn.save)
dn.reload()
dn.items[0].so_detail = None
self.assertRaises(frappe.ValidationError, dn.save)
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order(po_no="12345")
@@ -893,7 +898,7 @@ class TestDeliveryNote(FrappeTestCase):
"Cost of Goods Sold - TCP1": {"cost_center": cost_center},
stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_delivery_note_cost_center_with_balance_sheet_account(self):
@@ -922,7 +927,7 @@ class TestDeliveryNote(FrappeTestCase):
"Cost of Goods Sold - TCP1": {"cost_center": cost_center},
stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_make_sales_invoice_from_dn_for_returned_qty(self):
@@ -988,9 +993,7 @@ class TestDeliveryNote(FrappeTestCase):
},
)
make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
make_stock_entry(
item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42
)
make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42)
try:
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
@@ -999,9 +1002,7 @@ class TestDeliveryNote(FrappeTestCase):
self.fail("Batch numbers not getting added to bundled items in DN.")
raise e
self.assertTrue(
"TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item"
)
self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
@@ -1176,9 +1177,7 @@ class TestDeliveryNote(FrappeTestCase):
warehouse=warehouse,
target_warehouse=target,
)
self.assertFalse(
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
)
self.assertFalse(frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}))
def test_batch_expiry_for_delivery_note(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -1253,11 +1252,9 @@ class TestDeliveryNote(FrappeTestCase):
).name
# Step - 2: Inward Stock
se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3)
make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3)
serial_nos = (
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3)
.items[0]
.serial_no
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3).items[0].serial_no
)
# Step - 3: Create a Product Bundle
@@ -1332,6 +1329,126 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload()
self.assertFalse(dn.items[0].target_warehouse)
def test_sales_return_valuation_for_moving_average(self):
item_code = make_item(
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
).name
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=100.0,
posting_date=add_days(nowdate(), -5),
)
dn = create_delivery_note(item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4))
self.assertEqual(dn.items[0].incoming_rate, 100.0)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=200.0,
posting_date=add_days(nowdate(), -3),
)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=300.0,
posting_date=add_days(nowdate(), -2),
)
dn1 = create_delivery_note(
is_return=1,
item_code=item_code,
return_against=dn.name,
qty=-5,
rate=500,
company=dn.company,
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
do_not_submit=1,
posting_date=add_days(nowdate(), -1),
)
# (300 * 5) + (200 * 5) = 2500
# 2500 / 10 = 250
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
def test_sales_return_valuation_for_moving_average_case2(self):
# Make DN return
# Make Bakcdated Purchase Receipt and check DN return valuation rate
# The rate should be recalculate based on the backdated purchase receipt
frappe.flags.print_debug_messages = False
item_code = make_item(
"_Test Item Sales Return with MA Case2",
{"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"},
).name
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=100.0,
posting_date=add_days(nowdate(), -5),
)
dn = create_delivery_note(
item_code=item_code,
warehouse="_Test Warehouse - _TC",
qty=5,
rate=500,
posting_date=add_days(nowdate(), -4),
)
returned_dn = create_delivery_note(
is_return=1,
item_code=item_code,
return_against=dn.name,
qty=-5,
rate=500,
company=dn.company,
warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
posting_date=add_days(nowdate(), -1),
)
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0)
# Make backdated purchase receipt
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=200.0,
posting_date=add_days(nowdate(), -3),
)
returned_dn.reload()
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0)
def test_internal_transfer_for_non_stock_item(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
item = make_item(properties={"is_stock_item": 0}).name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 100
so = make_sales_order(item_code=item, qty=1, rate=rate, customer=customer, warehouse=warehouse)
dn = make_delivery_note(so.name)
dn.items[0].target_warehouse = target
dn.save().submit()
self.assertEqual(so.items[0].rate, rate)
self.assertEqual(dn.items[0].rate, so.items[0].rate)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -809,7 +809,8 @@
"label": "Purchase Order",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_82",
@@ -870,7 +871,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:37:38.638144",
"modified": "2024-03-21 18:15:07.603672",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Delivery Settings', {
refresh: function(frm) {
}
frappe.ui.form.on("Delivery Settings", {
refresh: function (frm) {},
});

View File

@@ -1,15 +1,15 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Delivery Trip', {
frappe.ui.form.on("Delivery Trip", {
setup: function (frm) {
frm.set_indicator_formatter('customer', (stop) => (stop.visited) ? "green" : "orange");
frm.set_indicator_formatter("customer", (stop) => (stop.visited ? "green" : "orange"));
frm.set_query("driver", function () {
return {
filters: {
"status": "Active"
}
status: "Active",
},
};
});
@@ -17,58 +17,71 @@ frappe.ui.form.on('Delivery Trip', {
var row = locals[cdt][cdn];
if (row.customer) {
return {
query: 'frappe.contacts.doctype.address.address.address_query',
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Customer",
link_name: row.customer
}
link_name: row.customer,
},
};
}
})
});
frm.set_query("contact", "delivery_stops", function (doc, cdt, cdn) {
var row = locals[cdt][cdn];
if (row.customer) {
return {
query: 'frappe.contacts.doctype.contact.contact.contact_query',
query: "frappe.contacts.doctype.contact.contact.contact_query",
filters: {
link_doctype: "Customer",
link_name: row.customer
}
link_name: row.customer,
},
};
}
})
});
},
refresh: function (frm) {
if (frm.doc.docstatus == 1 && frm.doc.delivery_stops.length > 0) {
frm.add_custom_button(__("Notify Customers via Email"), function () {
frm.trigger('notify_customers');
frm.trigger("notify_customers");
});
}
if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Delivery Note'), () => {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
source_doctype: "Delivery Note",
target: frm,
date_field: "posting_date",
setters: {
company: frm.doc.company,
},
get_query_filters: {
docstatus: 1,
company: frm.doc.company,
}
})
}, __("Get stops from"));
}
frm.add_custom_button(__("Delivery Notes"), function () {
frappe.set_route("List", "Delivery Note",
{'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]}
frm.add_custom_button(
__("Delivery Note"),
() => {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
source_doctype: "Delivery Note",
target: frm,
date_field: "posting_date",
setters: {
company: frm.doc.company,
},
get_query_filters: {
docstatus: 1,
company: frm.doc.company,
},
});
},
__("Get stops from")
);
}, __("View"));
}
frm.add_custom_button(
__("Delivery Notes"),
function () {
frappe.set_route("List", "Delivery Note", {
name: [
"in",
frm.doc.delivery_stops.map((stop) => {
return stop.delivery_note;
}),
],
});
},
__("View")
);
},
calculate_arrival_time: function (frm) {
@@ -77,13 +90,17 @@ frappe.ui.form.on('Delivery Trip', {
}
frappe.show_alert({
message: "Calculating Arrival Times",
indicator: 'orange'
});
frm.call("process_route", {
optimize: false,
}, () => {
frm.reload_doc();
indicator: "orange",
});
frm.call(
"process_route",
{
optimize: false,
},
() => {
frm.reload_doc();
}
);
},
driver: function (frm) {
@@ -91,13 +108,13 @@ frappe.ui.form.on('Delivery Trip', {
frappe.call({
method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_driver_email",
args: {
driver: frm.doc.driver
driver: frm.doc.driver,
},
callback: (data) => {
frm.set_value("driver_email", data.message.email);
}
},
});
};
}
},
optimize_route: function (frm) {
@@ -106,23 +123,27 @@ frappe.ui.form.on('Delivery Trip', {
}
frappe.show_alert({
message: "Optimizing Route",
indicator: 'orange'
});
frm.call("process_route", {
optimize: true,
}, () => {
frm.reload_doc();
indicator: "orange",
});
frm.call(
"process_route",
{
optimize: true,
},
() => {
frm.reload_doc();
}
);
},
notify_customers: function (frm) {
$.each(frm.doc.delivery_stops || [], function (i, delivery_stop) {
if (!delivery_stop.delivery_note) {
frappe.msgprint({
"message": __("No Delivery Note selected for Customer {}", [delivery_stop.customer]),
"title": __("Warning"),
"indicator": "orange",
"alert": 1
message: __("No Delivery Note selected for Customer {}", [delivery_stop.customer]),
title: __("Warning"),
indicator: "orange",
alert: 1,
});
}
});
@@ -135,48 +156,45 @@ frappe.ui.form.on('Delivery Trip', {
frappe.call({
method: "erpnext.stock.doctype.delivery_trip.delivery_trip.notify_customers",
args: {
"delivery_trip": frm.doc.name
delivery_trip: frm.doc.name,
},
callback: function (r) {
if (!r.exc) {
frm.doc.email_notification_sent = true;
frm.refresh_field('email_notification_sent');
frm.refresh_field("email_notification_sent");
}
}
},
});
});
}
});
}
},
});
frappe.ui.form.on('Delivery Stop', {
frappe.ui.form.on("Delivery Stop", {
customer: function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (row.customer) {
frappe.call({
method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_contact_and_address",
args: { "name": row.customer },
args: { name: row.customer },
callback: function (r) {
if (r.message) {
if (r.message["shipping_address"]) {
frappe.model.set_value(cdt, cdn, "address", r.message["shipping_address"].parent);
}
else {
frappe.model.set_value(cdt, cdn, "address", '');
} else {
frappe.model.set_value(cdt, cdn, "address", "");
}
if (r.message["contact_person"]) {
frappe.model.set_value(cdt, cdn, "contact", r.message["contact_person"].parent);
} else {
frappe.model.set_value(cdt, cdn, "contact", "");
}
else {
frappe.model.set_value(cdt, cdn, "contact", '');
}
} else {
frappe.model.set_value(cdt, cdn, "address", "");
frappe.model.set_value(cdt, cdn, "contact", "");
}
else {
frappe.model.set_value(cdt, cdn, "address", '');
frappe.model.set_value(cdt, cdn, "contact", '');
}
}
},
});
}
},
@@ -186,12 +204,12 @@ frappe.ui.form.on('Delivery Stop', {
if (row.address) {
frappe.call({
method: "frappe.contacts.doctype.address.address.get_address_display",
args: { "address_dict": row.address },
args: { address_dict: row.address },
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, "customer_address", r.message);
}
}
},
});
} else {
frappe.model.set_value(cdt, cdn, "customer_address", "");
@@ -203,15 +221,15 @@ frappe.ui.form.on('Delivery Stop', {
if (row.contact) {
frappe.call({
method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_contact_display",
args: { "contact": row.contact },
args: { contact: row.contact },
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, "customer_contact", r.message);
}
}
},
});
} else {
frappe.model.set_value(cdt, cdn, "customer_contact", "");
}
}
},
});

View File

@@ -13,7 +13,7 @@ from frappe.utils import cint, get_datetime, get_link_to_form
class DeliveryTrip(Document):
def __init__(self, *args, **kwargs):
super(DeliveryTrip, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Google Maps returns distances in meters by default
self.default_distance_uom = (
@@ -67,9 +67,7 @@ class DeliveryTrip(Document):
delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`.
"""
delivery_notes = list(
set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)
)
delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note))
update_fields = {
"driver": self.driver,
@@ -315,14 +313,11 @@ def get_contact_display(contact):
"Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1
)
contact_info.html = (
""" <b>%(first_name)s %(last_name)s</b> <br> %(phone)s <br> %(mobile_no)s"""
% {
"first_name": contact_info.first_name,
"last_name": contact_info.last_name or "",
"phone": contact_info.phone or "",
"mobile_no": contact_info.mobile_no or "",
}
contact_info.html = """ <b>{first_name} {last_name}</b> <br> {phone} <br> {mobile_no}""".format(
first_name=contact_info.first_name,
last_name=contact_info.last_name or "",
phone=contact_info.phone or "",
mobile_no=contact_info.mobile_no or "",
)
return contact_info.html

View File

@@ -1,4 +1,4 @@
frappe.listview_settings['Delivery Trip'] = {
frappe.listview_settings["Delivery Trip"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (in_list(["Cancelled", "Draft"], doc.status)) {
@@ -8,5 +8,5 @@ frappe.listview_settings['Delivery Trip'] = {
} else if (doc.status === "Completed") {
return [__(doc.status), "green", "status,=," + doc.status];
}
}
},
};

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase

View File

@@ -1,43 +1,59 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Inventory Dimension', {
frappe.ui.form.on("Inventory Dimension", {
setup(frm) {
frm.trigger('set_query_on_fields');
frm.trigger("set_query_on_fields");
},
set_query_on_fields(frm) {
frm.set_query('reference_document', () => {
frm.set_query("reference_document", () => {
let invalid_doctypes = frappe.model.core_doctypes_list;
invalid_doctypes.push('Batch', 'Serial No', 'Warehouse', 'Item', 'Inventory Dimension',
'Accounting Dimension', 'Accounting Dimension Filter');
invalid_doctypes.push(
"Batch",
"Serial No",
"Warehouse",
"Item",
"Inventory Dimension",
"Accounting Dimension",
"Accounting Dimension Filter"
);
return {
filters: {
'istable': 0,
'issingle': 0,
'name': ['not in', invalid_doctypes]
}
istable: 0,
issingle: 0,
name: ["not in", invalid_doctypes],
},
};
});
frm.set_query('document_type', () => {
frm.set_query("document_type", () => {
return {
query: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_documents',
query: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_documents",
};
});
},
onload(frm) {
frm.trigger('render_traget_field');
frm.trigger("render_traget_field");
frm.trigger("set_parent_fields");
},
refresh(frm) {
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock'];
if (
frm.doc.__onload &&
frm.doc.__onload.has_stock_ledger &&
frm.doc.__onload.has_stock_ledger.length
) {
let allow_to_edit_fields = [
"disabled",
"fetch_from_parent",
"type_of_transaction",
"condition",
"mandatory_depends_on",
"validate_negative_stock",
];
frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
@@ -47,8 +63,8 @@ frappe.ui.form.on('Inventory Dimension', {
}
if (!frm.is_new()) {
frm.add_custom_button(__('Delete Dimension'), () => {
frm.trigger('delete_dimension');
frm.add_custom_button(__("Delete Dimension"), () => {
frm.trigger("delete_dimension");
});
}
},
@@ -62,39 +78,38 @@ frappe.ui.form.on('Inventory Dimension', {
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
} else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({
method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields',
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
args: {
child_doctype: frm.doc.document_type,
dimension_name: frm.doc.reference_document
dimension_name: frm.doc.reference_document,
},
callback: (r) => {
if (r.message && r.message.length) {
frm.set_df_property("fetch_from_parent", "options",
[""].concat(r.message));
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
} else {
frm.set_df_property("fetch_from_parent", "hidden", 1);
}
}
},
});
}
},
delete_dimension(frm) {
let msg = (`
let msg = `
Custom fields related to this dimension will be deleted on deletion of dimension.
<br> Do you want to delete {0} dimension?
`);
`;
frappe.confirm(__(msg, [frm.doc.name.bold()]), () => {
frappe.call({
method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension',
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension",
args: {
dimension: frm.doc.name
dimension: frm.doc.name,
},
callback: function () {
frappe.set_route("List", "Inventory Dimension");
},
callback: function() {
frappe.set_route('List', 'Inventory Dimension');
}
});
});
}
},
});

View File

@@ -281,7 +281,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
dimensions = get_document_wise_inventory_dimensions(doc.doctype)
filter_dimensions = []
for row in dimensions:
if row.type_of_transaction:
if row.type_of_transaction and row.type_of_transaction != "Both":
if (
row.type_of_transaction == "Inward"
if doc.docstatus == 1
@@ -360,9 +360,7 @@ def delete_dimension(dimension):
@frappe.whitelist()
def get_parent_fields(child_doctype, dimension_name):
parent_doctypes = frappe.get_all(
"DocField", fields=["parent"], filters={"options": child_doctype}
)
parent_doctypes = frappe.get_all("DocField", fields=["parent"], filters={"options": child_doctype})
fields = []

View File

@@ -210,9 +210,7 @@ class TestInventoryDimension(FrappeTestCase):
)
self.assertFalse(
frappe.db.get_value(
"Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name"
)
frappe.db.get_value("Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name")
)
def test_check_mandatory_dimensions(self):
@@ -296,9 +294,7 @@ class TestInventoryDimension(FrappeTestCase):
se_doc.save()
se_doc.submit()
entries = get_voucher_sl_entries(
se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"]
)
entries = get_voucher_sl_entries(se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"])
for entry in entries:
self.assertEqual(entry.warehouse, warehouse)
@@ -429,6 +425,14 @@ class TestInventoryDimension(FrappeTestCase):
)
warehouse = create_warehouse("Negative Stock Warehouse")
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
self.assertRaises(frappe.ValidationError, doc.submit)
doc.reload()
if doc.docstatus == 1:
doc.cancel()
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
doc.items[0].to_inv_site = "Site 1"
@@ -480,7 +484,14 @@ def create_store_dimension():
"autoname": "field:store_name",
"fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
{
"role": "System Manager",
"permlevel": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
}
],
}
).insert(ignore_permissions=True)
@@ -502,7 +513,14 @@ def prepare_test_data():
"autoname": "field:shelf_name",
"fields": [{"label": "Shelf Name", "fieldname": "shelf_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
{
"role": "System Manager",
"permlevel": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
}
],
}
).insert(ignore_permissions=True)
@@ -524,7 +542,14 @@ def prepare_test_data():
"autoname": "field:rack_name",
"fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
{
"role": "System Manager",
"permlevel": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
}
],
}
).insert(ignore_permissions=True)
@@ -546,7 +571,14 @@ def prepare_test_data():
"autoname": "field:pallet_name",
"fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
{
"role": "System Manager",
"permlevel": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
}
],
}
).insert(ignore_permissions=True)
@@ -562,7 +594,14 @@ def prepare_test_data():
"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}
{
"role": "System Manager",
"permlevel": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
}
],
}
).insert(ignore_permissions=True)
@@ -615,9 +654,7 @@ def prepare_data_for_internal_transfer():
to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company)
pr_doc = make_purchase_receipt(
company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True
)
pr_doc = make_purchase_receipt(company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True)
pr_doc.items[0].store = "Inter Transfer Store 1"
pr_doc.submit()
@@ -643,9 +680,7 @@ def prepare_data_for_internal_transfer():
expense_account = frappe.db.get_value(
"Company", company, "stock_adjustment_account"
) or frappe.db.get_value(
"Account", {"company": company, "account_type": "Expense Account"}, "name"
)
) or frappe.db.get_value("Account", {"company": company, "account_type": "Expense Account"}, "name")
return frappe._dict(
{

File diff suppressed because it is too large Load Diff

View File

@@ -203,6 +203,7 @@
"label": "Allow Alternative Item"
},
{
"allow_in_quick_entry": 1,
"bold": 1,
"default": "1",
"depends_on": "eval:!doc.is_fixed_asset",
@@ -240,6 +241,7 @@
"label": "Standard Selling Rate"
},
{
"allow_in_quick_entry": 1,
"default": "0",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
@@ -247,6 +249,7 @@
"set_only_once": 1
},
{
"allow_in_quick_entry": 1,
"depends_on": "is_fixed_asset",
"fieldname": "asset_category",
"fieldtype": "Link",
@@ -897,7 +900,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-09-18 15:41:32.688051",
"modified": "2024-01-08 18:09:30.225085",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -3,7 +3,6 @@
import copy
import json
from typing import Dict, List, Optional
import frappe
from frappe import _
@@ -300,9 +299,9 @@ class Item(Document):
for d in self.get("uoms"):
if cstr(d.uom) in check_list:
frappe.throw(
_("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(
d.uom
)
_(
"Unit of Measure {0} has been entered more than once in Conversion Factor Table"
).format(d.uom)
)
else:
check_list.append(cstr(d.uom))
@@ -354,7 +353,7 @@ class Item(Document):
frappe.throw(
_("{0} entered twice {1} in Item Taxes").format(
frappe.bold(d.item_tax_template),
"for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
f"for tax category {frappe.bold(d.tax_category)}" if d.tax_category else "",
)
)
else:
@@ -373,7 +372,9 @@ class Item(Document):
)
if duplicate:
frappe.throw(
_("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0])
_("Barcode {0} already used in Item {1}").format(
item_barcode.barcode, duplicate[0][0]
)
)
item_barcode.barcode_type = (
@@ -403,9 +404,9 @@ class Item(Document):
warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
else:
frappe.throw(
_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
d.idx, d.warehouse, d.material_request_type
),
_(
"Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}."
).format(d.idx, d.warehouse, d.material_request_type),
DuplicateReorderRows,
)
@@ -477,20 +478,21 @@ class Item(Document):
for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"):
for d in frappe.db.sql(
"""select name, item_wise_tax_detail from `tab{0}`
where ifnull(item_wise_tax_detail, '') != ''""".format(
dt
),
f"""select name, item_wise_tax_detail from `tab{dt}`
where ifnull(item_wise_tax_detail, '') != ''""",
as_dict=1,
):
item_wise_tax_detail = json.loads(d.item_wise_tax_detail)
if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail:
item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name]
item_wise_tax_detail.pop(old_name)
frappe.db.set_value(
dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False
dt,
d.name,
"item_wise_tax_detail",
json.dumps(item_wise_tax_detail),
update_modified=False,
)
def delete_old_bins(self, old_name):
@@ -517,9 +519,7 @@ class Item(Document):
)
msg += " <br>"
msg += (
", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
)
msg += ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
msg += _(
"Note: To merge the items, create a separate Stock Reconciliation for the old item {0}"
@@ -542,12 +542,8 @@ class Item(Document):
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
old_bundle = frappe.get_value(
"Product Bundle", filters={"new_item_code": old_name, "disabled": 0}
)
new_bundle = frappe.get_value(
"Product Bundle", filters={"new_item_code": new_name, "disabled": 0}
)
old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name, "disabled": 0})
new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name, "disabled": 0})
if old_bundle and new_bundle:
bundle_link = get_link_to_form("Product Bundle", old_bundle)
@@ -572,7 +568,7 @@ class Item(Document):
if len(web_items) <= 1:
return
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
old_web_item = next(d.get("name") for d in web_items if d.get("item_code") == old_name)
web_item_link = get_link_to_form("Website Item", old_web_item)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
@@ -586,9 +582,7 @@ class Item(Document):
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
existing_allow_negative_stock = frappe.db.get_value(
"Stock Settings", None, "allow_negative_stock"
)
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
repost_stock_for_warehouses = frappe.get_all(
@@ -605,9 +599,7 @@ class Item(Document):
for warehouse in repost_stock_for_warehouses:
repost_stock(new_name, warehouse)
frappe.db.set_value(
"Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock
)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
def update_bom_item_desc(self):
if self.is_new():
@@ -768,16 +760,14 @@ class Item(Document):
return "<br>".join(docnames)
def table_row(title, body):
return """<tr>
<td>{0}</td>
<td>{1}</td>
</tr>""".format(
title, body
)
return f"""<tr>
<td>{title}</td>
<td>{body}</td>
</tr>"""
rows = ""
for docname, attr_list in not_included.items():
link = "<a href='/app/Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
link = f"<a href='/app/Form/Item/{frappe.bold(_(docname))}'>{frappe.bold(_(docname))}</a>"
rows += table_row(link, body(attr_list))
error_description = _(
@@ -785,17 +775,15 @@ class Item(Document):
)
message = """
<div>{0}</div><br>
<div>{}</div><br>
<table class="table">
<thead>
<td>{1}</td>
<td>{2}</td>
<td>{}</td>
<td>{}</td>
</thead>
{3}
{}
</table>
""".format(
error_description, _("Variant Items"), _("Attributes"), rows
)
""".format(error_description, _("Variant Items"), _("Attributes"), rows)
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True)
@@ -925,7 +913,7 @@ class Item(Document):
frappe.throw(msg, title=_("Linked with submitted documents"))
def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]:
def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None:
linked_doctypes = [
"Delivery Note Item",
"Sales Invoice Item",
@@ -1047,6 +1035,7 @@ def validate_cancelled_item(item_code, docstatus=None):
frappe.throw(_("Item {0} is cancelled").format(item_code))
@frappe.request_cache
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom"""
# get last purchase order item details
@@ -1087,17 +1076,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01"
)
if last_purchase_order and (
purchase_order_date >= purchase_receipt_date or not last_purchase_receipt
):
if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt):
# use purchase order
last_purchase = last_purchase_order[0]
purchase_date = purchase_order_date
elif last_purchase_receipt and (
purchase_receipt_date > purchase_order_date or not last_purchase_order
):
elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order):
# use purchase receipt
last_purchase = last_purchase_receipt[0]
purchase_date = purchase_receipt_date
@@ -1304,7 +1289,7 @@ def set_item_tax_from_hsn_code(item):
pass
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
def validate_item_default_company_links(item_defaults: list[ItemDefault]) -> None:
for item_default in item_defaults:
for doctype, field in [
["Warehouse", "default_warehouse"],

View File

@@ -1,9 +1,8 @@
frappe.listview_settings['Item'] = {
add_fields: ["item_name", "stock_uom", "item_group", "image",
"has_variants", "end_of_life", "disabled"],
frappe.listview_settings["Item"] = {
add_fields: ["item_name", "stock_uom", "item_group", "image", "has_variants", "end_of_life", "disabled"],
filters: [["disabled", "=", "0"]],
get_indicator: function(doc) {
get_indicator: function (doc) {
if (doc.disabled) {
return [__("Disabled"), "grey", "disabled,=,Yes"];
} else if (doc.end_of_life && doc.end_of_life < frappe.datetime.get_today()) {
@@ -17,24 +16,23 @@ frappe.listview_settings['Item'] = {
reports: [
{
name: 'Stock Summary',
report_type: 'Page',
route: 'stock-balance'
name: "Stock Summary",
report_type: "Page",
route: "stock-balance",
},
{
name: 'Stock Ledger',
report_type: 'Script Report'
name: "Stock Ledger",
report_type: "Script Report",
},
{
name: 'Stock Balance',
report_type: 'Script Report'
name: "Stock Balance",
report_type: "Script Report",
},
{
name: 'Stock Projected Qty',
report_type: 'Script Report'
}
]
name: "Stock Projected Qty",
report_type: "Script Report",
},
],
};
frappe.help.youtube_id["Item"] = "qXaEwld4_Ps";

View File

@@ -32,7 +32,7 @@ test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
def make_item(item_code=None, properties=None, uoms=None):
def make_item(item_code=None, properties=None, uoms=None, barcode=None):
if not item_code:
item_code = frappe.generate_hash(length=16)
@@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None):
for uom in uoms:
item.append("uoms", uom)
if barcode:
item.append(
"barcodes",
{
"barcode": barcode,
},
)
item.insert()
return item
@@ -315,7 +323,6 @@ class TestItem(FrappeTestCase):
self.assertEqual(value, purchase_item_details.get(key))
def test_item_default_validations(self):
with self.assertRaises(frappe.ValidationError) as ve:
make_item(
"Bad Item defaults",
@@ -469,9 +476,7 @@ class TestItem(FrappeTestCase):
self.assertFalse(frappe.db.exists("Item", old))
self.assertTrue(
frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"})
)
self.assertTrue(frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}))
self.assertTrue(
frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})
)
@@ -524,7 +529,7 @@ class TestItem(FrappeTestCase):
def test_item_variant_by_manufacturer(self):
template = make_item(
"_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
)
).name
for manufacturer in ["DFSS", "DASA", "ASAAS"]:
if not frappe.db.exists("Manufacturer", manufacturer):
@@ -715,9 +720,7 @@ class TestItem(FrappeTestCase):
@change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"})
def test_retain_sample(self):
item = make_item(
"_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1}
)
item = make_item("_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1})
self.assertEqual(item.has_batch_no, 1)
self.assertEqual(item.retain_sample, 1)
@@ -790,7 +793,7 @@ class TestItem(FrappeTestCase):
def test_customer_codes_length(self):
"""Check if item code with special characters are allowed."""
item = make_item(properties={"item_code": "Test Item Code With Special Characters"})
for row in range(3):
for _row in range(3):
item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)})
item.save()
self.assertTrue(len(item.customer_code) > 140)
@@ -831,9 +834,7 @@ class TestItem(FrappeTestCase):
make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype")
item = make_item(properties={"item_name": "Test Item", "description": "Test Description"})
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertTrue("description" not in data[0])
@@ -841,9 +842,7 @@ class TestItem(FrappeTestCase):
make_property_setter(
"Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype"
)
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertEqual(data[0].description, item.description)

View File

@@ -1,14 +1,14 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Item Alternative', {
setup: function(frm) {
frappe.ui.form.on("Item Alternative", {
setup: function (frm) {
frm.fields_dict.item_code.get_query = () => {
return {
filters: {
'allow_alternative_item': 1
}
allow_alternative_item: 1,
},
};
};
}
},
});

View File

@@ -30,9 +30,7 @@ class ItemAlternative(Document):
"allow_alternative_item",
]
item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1)
alternative_item_data = frappe.db.get_value(
"Item", self.alternative_item_code, fields, as_dict=1
)
alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1)
for field in fields:
if item_data.get(field) != alternative_item_data.get(field):
@@ -72,14 +70,12 @@ class ItemAlternative(Document):
@frappe.validate_and_sanitize_search_inputs
def get_alternative_items(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
""" (select alternative_item_code from `tabItem Alternative`
f""" (select alternative_item_code from `tabItem Alternative`
where item_code = %(item_code)s and alternative_item_code like %(txt)s)
union
(select item_code from `tabItem Alternative`
where alternative_item_code = %(item_code)s and item_code like %(txt)s
and two_way = 1) limit {1} offset {0}
""".format(
start, page_len
),
and two_way = 1) limit {page_len} offset {start}
""",
{"item_code": filters.get("item_code"), "txt": "%" + txt + "%"},
)

View File

@@ -54,9 +54,7 @@ class TestItemAlternative(FrappeTestCase):
"fg_item_qty": 5,
},
]
sco = get_subcontracting_order(
service_items=service_items, supplier_warehouse=supplier_warehouse
)
sco = get_subcontracting_order(service_items=service_items, supplier_warehouse=supplier_warehouse)
rm_items = [
{
"item_code": "Test Finished Goods - A",
@@ -106,9 +104,7 @@ class TestItemAlternative(FrappeTestCase):
"reserved_qty_for_sub_contract",
)
self.assertEqual(
after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
)
self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5))
scr = make_subcontracting_receipt(sco.name)
scr.save()
@@ -159,9 +155,7 @@ class TestItemAlternative(FrappeTestCase):
"reserved_qty_for_production",
)
self.assertEqual(
reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)
)
self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5))
ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5))
status = False

View File

@@ -1,6 +1,4 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Item Attribute', {
});
frappe.ui.form.on("Item Attribute", {});

View File

@@ -75,7 +75,9 @@ class ItemAttribute(Document):
values, abbrs = [], []
for d in self.item_attribute_values:
if d.attribute_value.lower() in map(str.lower, values):
frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
frappe.throw(
_("Attribute value: {0} must appear only once").format(d.attribute_value.title())
)
values.append(d.attribute_value)
if d.abbr.lower() in map(str.lower, abbrs):

View File

@@ -1,8 +1,7 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Item Manufacturer', {
frappe.ui.form.on("Item Manufacturer", {
// refresh: function(frm) {
// }
});

View File

@@ -41,7 +41,9 @@ class ItemManufacturer(Document):
# if unchecked and default in Item master, clear it.
if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no:
frappe.db.set_value(
"Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None}
"Item",
item.name,
{"default_item_manufacturer": None, "default_manufacturer_part_no": None},
)
elif self.is_default:

View File

@@ -3,11 +3,11 @@
frappe.ui.form.on("Item Price", {
setup(frm) {
frm.set_query("item_code", function() {
frm.set_query("item_code", function () {
return {
filters: {
"has_variants": 0
}
has_variants: 0,
},
};
});
},
@@ -23,15 +23,18 @@ frappe.ui.form.on("Item Price", {
frm.add_fetch("item_code", "description", "item_description");
frm.add_fetch("item_code", "stock_uom", "uom");
frm.set_df_property("bulk_import_help", "options",
'<a href="/app/data-import-tool/Item Price">' + __("Import in Bulk") + '</a>');
frm.set_df_property(
"bulk_import_help",
"options",
'<a href="/app/data-import-tool/Item Price">' + __("Import in Bulk") + "</a>"
);
frm.set_query('batch_no', function() {
frm.set_query("batch_no", function () {
return {
filters: {
'item': frm.doc.item_code
}
item: frm.doc.item_code,
},
};
});
}
},
});

View File

@@ -104,7 +104,8 @@
"in_standard_filter": 1,
"label": "Price List",
"options": "Price List",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
@@ -220,7 +221,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-15 08:26:04.041861",
"modified": "2024-03-13 12:23:39.630290",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Price",

View File

@@ -40,7 +40,7 @@ class ItemPrice(Document):
if not price_list_details:
link = frappe.utils.get_link_to_form("Price List", self.price_list)
frappe.throw("The price list {0} does not exist or is disabled".format(link))
frappe.throw(f"The price list {link} does not exist or is disabled")
self.buying, self.selling, self.currency = price_list_details
@@ -57,7 +57,6 @@ class ItemPrice(Document):
frappe.throw(_(msg))
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")
query = (

View File

@@ -1,3 +1,3 @@
frappe.listview_settings['Item Price'] = {
frappe.listview_settings["Item Price"] = {
hide_name_column: true,
};

View File

@@ -1,24 +1,39 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Item Variant Settings', {
refresh: function(frm) {
frappe.ui.form.on("Item Variant Settings", {
refresh: function (frm) {
const allow_fields = [];
const existing_fields = frm.doc.fields.map(row => row.field_name);
const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name",
"published_in_website", "standard_rate", "opening_stock", "image",
"variant_of", "valuation_rate", "barcodes", "has_variants", "attributes"];
const existing_fields = frm.doc.fields.map((row) => row.field_name);
const exclude_fields = [
...existing_fields,
"naming_series",
"item_code",
"item_name",
"published_in_website",
"standard_rate",
"opening_stock",
"image",
"variant_of",
"valuation_rate",
"barcodes",
"has_variants",
"attributes",
];
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
const exclude_field_types = ["HTML", "Section Break", "Column Break", "Button", "Read Only"];
frappe.model.with_doctype('Item', () => {
frappe.model.with_doctype("Item", () => {
const field_label_map = {};
frappe.get_meta('Item').fields.forEach(d => {
frappe.get_meta("Item").fields.forEach((d) => {
field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`;
if (!in_list(exclude_field_types, d.fieldtype)
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
if (
!in_list(exclude_field_types, d.fieldtype) &&
!d.no_copy &&
!in_list(exclude_fields, d.fieldname)
) {
allow_fields.push({
label: field_label_map[d.fieldname],
value: d.fieldname,
@@ -33,9 +48,7 @@ frappe.ui.form.on('Item Variant Settings', {
});
}
frm.fields_dict.fields.grid.update_docfield_property(
'field_name', 'options', allow_fields
);
frm.fields_dict.fields.grid.update_docfield_property("field_name", "options", allow_fields);
});
}
},
});

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import typing
import frappe
from frappe import _
@@ -8,7 +8,7 @@ from frappe.model.document import Document
class ItemVariantSettings(Document):
invalid_fields_for_copy_fields_in_variants = ["barcodes"]
invalid_fields_for_copy_fields_in_variants: typing.ClassVar[list] = ["barcodes"]
def set_default_fields(self):
self.fields = []
@@ -24,7 +24,6 @@ class ItemVariantSettings(Document):
"description",
"variant_of",
"valuation_rate",
"description",
"barcodes",
"has_variants",
"attributes",
@@ -50,4 +49,6 @@ class ItemVariantSettings(Document):
def validate(self):
for d in self.fields:
if d.field_name in self.invalid_fields_for_copy_fields_in_variants:
frappe.throw(_("Cannot set the field <b>{0}</b> for copying in variants").format(d.field_name))
frappe.throw(
_("Cannot set the field <b>{0}</b> for copying in variants").format(d.field_name)
)

View File

@@ -38,6 +38,7 @@ class LandedCostVoucher(Document):
def validate(self):
self.check_mandatory()
self.validate_receipt_documents()
self.validate_line_items()
init_landed_taxes_and_totals(self)
self.set_total_taxes_and_charges()
if not self.get("items"):
@@ -45,6 +46,26 @@ class LandedCostVoucher(Document):
self.set_applicable_charges_on_item()
def validate_line_items(self):
for d in self.get("items"):
if (
d.docstatus == 0
and d.purchase_receipt_item
and not frappe.db.exists(
d.receipt_document_type + " Item",
{"name": d.purchase_receipt_item, "parent": d.receipt_document},
)
):
frappe.throw(
_("Row {0}: {2} Item {1} does not exist in {2} {3}").format(
d.idx,
frappe.bold(d.purchase_receipt_item),
d.receipt_document_type,
frappe.bold(d.receipt_document),
),
title=_("Incorrect Reference Document (Purchase Receipt Item)"),
)
def check_mandatory(self):
if not self.get("purchase_receipts"):
frappe.throw(_("Please enter Receipt Document"))
@@ -55,13 +76,13 @@ class LandedCostVoucher(Document):
for d in self.get("purchase_receipts"):
docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
if docstatus != 1:
msg = (
f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
)
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
frappe.throw(_(msg), title=_("Invalid Document"))
if d.receipt_document_type == "Purchase Invoice":
update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock")
update_stock = frappe.db.get_value(
d.receipt_document_type, d.receipt_document, "update_stock"
)
if not update_stock:
msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(
d.idx, frappe.bold(d.receipt_document)
@@ -111,7 +132,8 @@ class LandedCostVoucher(Document):
)
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
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
@@ -122,6 +144,13 @@ class LandedCostVoucher(Document):
self.get("items")[item_count - 1].applicable_charges += diff
def validate_applicable_charges_for_item(self):
if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1:
frappe.throw(
_(
"Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher."
)
)
based_on = self.distribute_charges_based_on.lower()
if based_on != "distribute manually":
@@ -167,7 +196,8 @@ class LandedCostVoucher(Document):
for d in self.get("purchase_receipts"):
doc = frappe.get_doc(d.receipt_document_type, d.receipt_document)
# check if there are {qty} assets created and linked to this receipt document
self.validate_asset_qty_and_status(d.receipt_document_type, doc)
if self.docstatus != 2:
self.validate_asset_qty_and_status(d.receipt_document_type, doc)
# set landed cost voucher amount in pr item
doc.set_landed_cost_voucher_amount()
@@ -201,28 +231,28 @@ class LandedCostVoucher(Document):
for item in self.get("items"):
if item.is_fixed_asset:
receipt_document_type = (
"purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt"
"purchase_invoice"
if item.receipt_document_type == "Purchase Invoice"
else "purchase_receipt"
)
docs = frappe.db.get_all(
"Asset",
filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
fields=["name", "docstatus"],
)
if not docs or len(docs) != item.qty:
if not docs or len(docs) < item.qty:
frappe.throw(
_(
"There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document."
).format(item.receipt_document, item.qty)
"There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
).format(len(docs), item.receipt_document, item.qty)
)
if docs:
for d in docs:
if d.docstatus == 1:
frappe.throw(
_(
"{2} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> from table to continue."
).format(
item.receipt_document, item.item_code, item.receipt_document_type
)
"{0} <b>{1}</b> has submitted Assets. Remove Item <b>{2}</b> from table to continue."
).format(item.receipt_document_type, item.receipt_document, item.item_code)
)
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
@@ -231,10 +261,10 @@ class LandedCostVoucher(Document):
serial_nos = get_serial_nos(item.serial_no)
if serial_nos:
frappe.db.sql(
"update `tabSerial No` set purchase_rate=%s where name in ({0})".format(
"update `tabSerial No` set purchase_rate=%s where name in ({})".format(
", ".join(["%s"] * len(serial_nos))
),
tuple([item.valuation_rate] + serial_nos),
tuple([item.valuation_rate, *serial_nos]),
)

View File

@@ -62,9 +62,7 @@ class TestLandedCostVoucher(FrappeTestCase):
as_dict=1,
)
self.assertEqual(
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
# assert after submit
@@ -87,7 +85,6 @@ class TestLandedCostVoucher(FrappeTestCase):
self.assertPurchaseReceiptLCVGLEntries(pr)
def assertPurchaseReceiptLCVGLEntries(self, pr):
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -170,9 +167,7 @@ class TestLandedCostVoucher(FrappeTestCase):
as_dict=1,
)
self.assertEqual(
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_for_zero_purchase_rate(self):
@@ -229,7 +224,6 @@ class TestLandedCostVoucher(FrappeTestCase):
)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
update_stock=1,
posting_date=frappe.utils.nowdate(),
@@ -274,9 +268,7 @@ class TestLandedCostVoucher(FrappeTestCase):
as_dict=1,
)
self.assertEqual(
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
@@ -365,9 +357,7 @@ class TestLandedCostVoucher(FrappeTestCase):
new_purchase_rate = serial_no_rate + charges
serial_no = frappe.db.get_value(
"Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1
)
serial_no = frappe.db.get_value("Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1)
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
@@ -392,7 +382,7 @@ class TestLandedCostVoucher(FrappeTestCase):
do_not_save=True,
)
pr.items[0].cost_center = "Main - TCP1"
for x in range(2):
for _x in range(2):
pr.append(
"items",
{

View File

@@ -1,16 +1,15 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Manufacturer', {
refresh: function(frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Manufacturer' };
frappe.ui.form.on("Manufacturer", {
refresh: function (frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: "Manufacturer" };
if (frm.doc.__islocal) {
hide_field(['address_html','contact_html']);
hide_field(["address_html", "contact_html"]);
frappe.contacts.clear_address_and_contact(frm);
}
else {
unhide_field(['address_html','contact_html']);
} else {
unhide_field(["address_html", "contact_html"]);
frappe.contacts.render_address_and_contact(frm);
}
}
},
});

View File

@@ -199,9 +199,9 @@ frappe.ui.form.on('Material Request', {
get_item_data: function(frm, item, overwrite_warehouse=false) {
if (item && !item.item_code) { return; }
frm.call({
frappe.call({
method: "erpnext.stock.get_item_details.get_item_details",
child: item,
args: {
args: {
item_code: item.item_code,
@@ -226,12 +226,22 @@ frappe.ui.form.on('Material Request', {
},
callback: function(r) {
const d = item;
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
const allow_to_change_fields = ['actual_qty', 'projected_qty', 'min_order_qty', 'item_name', 'description', 'stock_uom', 'uom', 'conversion_factor', 'stock_qty'];
if(!r.exc) {
$.each(r.message, function(k, v) {
if(!d[k] || in_list(qty_fields, k)) d[k] = v;
$.each(r.message, function(key, value) {
if(!d[key] || allow_to_change_fields.includes(key)) {
d[key] = value;
}
});
if (d.price_list_rate != r.message.price_list_rate) {
d.rate = 0.0;
d.price_list_rate = r.message.price_list_rate;
frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate);
}
refresh_field("items");
}
}
});
@@ -243,7 +253,7 @@ frappe.ui.form.on('Material Request', {
fields: [
{"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"),
options:"BOM", reqd: 1, get_query: function() {
return {filters: { docstatus:1 }};
return {filters: { docstatus:1, "is_active": 1 }};
}},
{"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"),
options:"Warehouse", reqd: 1},
@@ -428,9 +438,11 @@ frappe.ui.form.on("Material Request Item", {
frm.events.get_item_data(frm, item, false);
},
rate: function(frm, doctype, name) {
rate(frm, doctype, name) {
const item = locals[doctype][name];
frm.events.get_item_data(frm, item, false);
item.amount = flt(item.qty) * flt(item.rate);
frappe.model.set_value(doctype, name, "amount", item.amount);
refresh_field("amount", item.name, item.parentfield);
},
item_code: function(frm, doctype, name) {
@@ -450,7 +462,12 @@ frappe.ui.form.on("Material Request Item", {
set_schedule_date(frm);
}
}
}
},
conversion_factor: function(frm, doctype, name) {
const item = locals[doctype][name];
frm.events.get_item_data(frm, item, false);
},
});
erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController {
@@ -515,6 +532,13 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten
schedule_date() {
set_schedule_date(this.frm);
}
qty(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
row.amount = flt(row.qty) * flt(row.rate);
frappe.model.set_value(cdt, cdn, "amount", row.amount);
refresh_field("amount", row.name, row.parentfield);
}
};
// for backward compatibility: combine new and previous states

View File

@@ -33,10 +33,10 @@ class MaterialRequest(BuyingController):
so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}}
for d in self.get("items"):
if d.sales_order:
if not d.sales_order in so_items:
if d.sales_order not in so_items:
so_items[d.sales_order] = {d.item_code: flt(d.qty)}
else:
if not d.item_code in so_items[d.sales_order]:
if d.item_code not in so_items[d.sales_order]:
so_items[d.sales_order][d.item_code] = flt(d.qty)
else:
so_items[d.sales_order][d.item_code] += flt(d.qty)
@@ -61,13 +61,13 @@ class MaterialRequest(BuyingController):
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
frappe.throw(
_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(
actual_so_qty - already_indented, item, so_no
)
_(
"Material Request of maximum {0} can be made for Item {1} against Sales Order {2}"
).format(actual_so_qty - already_indented, item, so_no)
)
def validate(self):
super(MaterialRequest, self).validate()
super().validate()
self.validate_schedule_date()
self.check_for_on_hold_or_closed_status("Sales Order", "sales_order")
@@ -123,7 +123,9 @@ class MaterialRequest(BuyingController):
def on_submit(self):
self.update_requested_qty_in_production_plan()
self.update_requested_qty()
if self.material_request_type == "Purchase":
if self.material_request_type == "Purchase" and frappe.db.exists(
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
):
self.validate_budget()
def before_save(self):
@@ -139,12 +141,8 @@ class MaterialRequest(BuyingController):
self.set_status(update=True, status="Cancelled")
def check_modified_date(self):
mod_db = frappe.db.sql(
"""select modified from `tabMaterial Request` where name = %s""", self.name
)
date_diff = frappe.db.sql(
"""select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified))
)
mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name)
date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""")
if date_diff and date_diff[0][0]:
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name))
@@ -324,9 +322,7 @@ def update_completed_and_requested_qty(stock_entry, method):
def set_missing_values(source, target_doc):
if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(
nowdate()
):
if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
target_doc.schedule_date = None
target_doc.run_method("set_missing_values")
target_doc.run_method("calculate_taxes_and_totals")
@@ -415,6 +411,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
postprocess,
)
doclist.set_onload("load_after_mapping", False)
return doclist
@@ -455,9 +452,7 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non
target_doc.schedule_date = None
target_doc.set(
"items",
[
d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0
],
[d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0],
)
set_missing_values(source, target_doc)
@@ -509,7 +504,7 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
conditions += "and mr.transaction_date between '{0}' and '{1}' ".format(date[0], date[1])
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
supplier = filters.get("supplier")
supplier_items = get_items_based_on_default_supplier(supplier)
@@ -521,18 +516,18 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
"""select distinct mr.name, transaction_date,company
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr.name = mr_item.parent
and mr_item.item_code in ({0})
and mr_item.item_code in ({})
and mr.material_request_type = 'Purchase'
and mr.per_ordered < 99.99
and mr.docstatus = 1
and mr.status != 'Stopped'
and mr.company = %s
{1}
{}
order by mr_item.item_code ASC
limit {2} offset {3} """.format(
limit {} offset {} """.format(
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
),
tuple(supplier_items) + (filters.get("company"),),
(*tuple(supplier_items), filters.get("company")),
as_dict=1,
)
@@ -547,17 +542,28 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte
for d in doc.items:
item_list.append(d.item_code)
return frappe.db.sql(
"""select default_supplier
from `tabItem Default`
where parent in ({0}) and
default_supplier IS NOT NULL
""".format(
", ".join(["%s"] * len(item_list))
),
tuple(item_list),
supplier = frappe.qb.DocType("Supplier")
item_default = frappe.qb.DocType("Item Default")
query = (
frappe.qb.from_(supplier)
.left_join(item_default)
.on(supplier.name == item_default.default_supplier)
.select(item_default.default_supplier)
.where(
(item_default.parent.isin(item_list))
& (item_default.default_supplier.notnull())
& (supplier[searchfield].like(f"%{txt}%"))
)
.offset(start)
.limit(page_len)
)
meta = frappe.get_meta("Supplier")
if meta.show_title_field_in_link and meta.title_field:
query = query.select(supplier[meta.title_field])
return query.run(as_dict=False)
@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
@@ -649,7 +655,10 @@ def make_stock_entry(source_name, target_doc=None):
"doctype": "Stock Entry",
"validation": {
"docstatus": ["=", 1],
"material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]],
"material_request_type": [
"in",
["Material Transfer", "Material Issue", "Customer Provided"],
],
},
},
"Material Request Item": {
@@ -679,9 +688,7 @@ def raise_work_orders(material_request):
mr = frappe.get_doc("Material Request", material_request)
errors = []
work_orders = []
default_wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse"
)
default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
for d in mr.items:
if (d.stock_qty - d.ordered_qty) > 0:

View File

@@ -1,8 +1,8 @@
frappe.listview_settings['Material Request'] = {
frappe.listview_settings["Material Request"] = {
add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"],
get_indicator: function(doc) {
get_indicator: function (doc) {
var precision = frappe.defaults.get_default("float_precision");
if (doc.status=="Stopped") {
if (doc.status == "Stopped") {
return [__("Stopped"), "red", "status,=,Stopped"];
} else if (doc.transfer_status && doc.docstatus != 2) {
if (doc.transfer_status == "Not Started") {
@@ -12,12 +12,16 @@ frappe.listview_settings['Material Request'] = {
} else if (doc.transfer_status == "Completed") {
return [__("Completed"), "green"];
}
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) {
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0"];
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) {
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
} else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) {
if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) {
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
if (
doc.material_request_type == "Purchase" &&
flt(doc.per_received, precision) < 100 &&
flt(doc.per_received, precision) > 0
) {
return [__("Partially Received"), "yellow", "per_received,<,100"];
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
return [__("Received"), "green", "per_received,=,100"];
@@ -33,5 +37,5 @@ frappe.listview_settings['Material Request'] = {
return [__("Manufactured"), "green", "per_ordered,=,100"];
}
}
}
},
};

View File

@@ -744,9 +744,7 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self):
create_item(
"CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
)
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided")
@@ -762,6 +760,62 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(mr.per_ordered, 100)
self.assertEqual(existing_requested_qty, current_requested_qty)
def test_auto_email_users_with_company_user_permissions(self):
from erpnext.stock.reorder_item import get_email_list
comapnywise_users = {
"_Test Company": "test_auto_email_@example.com",
"_Test Company 1": "test_auto_email_1@example.com",
}
permissions = []
for company, user in comapnywise_users.items():
if not frappe.db.exists("User", user):
frappe.get_doc(
{
"doctype": "User",
"email": user,
"first_name": user,
"send_notifications": 0,
"enabled": 1,
"user_type": "System User",
"roles": [{"role": "Purchase Manager"}],
}
).insert(ignore_permissions=True)
if not frappe.db.exists(
"User Permission", {"user": user, "allow": "Company", "for_value": company}
):
perm_doc = frappe.get_doc(
{
"doctype": "User Permission",
"user": user,
"allow": "Company",
"for_value": company,
"apply_to_all_doctypes": 1,
}
).insert(ignore_permissions=True)
permissions.append(perm_doc)
comapnywise_mr_list = frappe._dict({})
mr1 = make_material_request()
comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name)
mr2 = make_material_request(
company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1"
)
comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)
for company, _mr_list in comapnywise_mr_list.items():
emails = get_email_list(company)
self.assertTrue(comapnywise_users[company] in emails)
for perm in permissions:
perm.delete()
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):
@@ -772,9 +826,7 @@ def get_in_transit_warehouse(company):
}
).insert()
in_transit_warehouse = frappe.db.exists(
"Warehouse", {"warehouse_type": "Transit", "company": company}
)
in_transit_warehouse = frappe.db.exists("Warehouse", {"warehouse_type": "Transit", "company": company})
if not in_transit_warehouse:
in_transit_warehouse = (

View File

@@ -35,6 +35,7 @@
"received_qty",
"rate_and_amount_section_break",
"rate",
"price_list_rate",
"col_break3",
"amount",
"accounting_details_section",
@@ -474,13 +475,22 @@
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
},
{
"fieldname": "price_list_rate",
"fieldtype": "Currency",
"hidden": 1,
"label": "Price List Rate",
"options": "currency",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:37:59.599115",
"modified": "2024-02-08 16:30:56.137858",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",

View File

@@ -23,9 +23,7 @@ def make_packing_list(doc):
return
parent_items_price, reset = {}, False
set_price_from_children = frappe.db.get_single_value(
"Selling Settings", "editable_bundle_item_rates"
)
set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates")
stale_packed_items_table = get_indexed_packed_items_table(doc)
@@ -244,9 +242,7 @@ def get_packed_item_bin_qty(item, warehouse):
def get_cancelled_doc_packed_item_details(old_packed_items):
prev_doc_packed_items_map = {}
for items in old_packed_items:
prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(
items.as_dict()
)
prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(items.as_dict())
return prev_doc_packed_items_map

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from typing import List, Optional, Tuple
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
@@ -15,8 +14,8 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
def create_product_bundle(
quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
) -> Tuple[str, List[str]]:
quantities: list[int] | None = None, warehouse: str | None = None
) -> tuple[str, list[str]]:
"""Get a new product_bundle for use in tests.
Create 10x required stock if warehouse is specified.
@@ -169,9 +168,7 @@ class TestPackedItem(FrappeTestCase):
# backdated stock entry
for item in self.bundle_items:
make_stock_entry(
item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday
)
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
# assert correct reposting
gles = get_gl_entries(dn.doctype, dn.name)
@@ -182,14 +179,15 @@ class TestPackedItem(FrappeTestCase):
def assertReturns(self, original, returned):
self.assertEqual(len(original), len(returned))
sort_function = lambda p: (p.parent_item, p.item_code, p.qty)
def sort_function(p):
return p.parent_item, p.item_code, p.qty
for sent, returned in zip(
sorted(original, key=sort_function), sorted(returned, key=sort_function)
for sent_item, returned_item in zip(
sorted(original, key=sort_function), sorted(returned, key=sort_function), strict=False
):
self.assertEqual(sent.item_code, returned.item_code)
self.assertEqual(sent.parent_item, returned.parent_item)
self.assertEqual(sent.qty, -1 * returned.qty)
self.assertEqual(sent_item.item_code, returned_item.item_code)
self.assertEqual(sent_item.parent_item, returned_item.parent_item)
self.assertEqual(sent_item.qty, -1 * returned_item.qty)
def test_returning_full_bundles(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return

View File

@@ -1,45 +1,45 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Packing Slip', {
setup: (frm) => {
frm.set_query('delivery_note', () => {
return {
filters: {
docstatus: 0,
}
}
});
frappe.ui.form.on("Packing Slip", {
setup: (frm) => {
frm.set_query("delivery_note", () => {
return {
filters: {
docstatus: 0,
},
};
});
frm.set_query('item_code', 'items', (doc, cdt, cdn) => {
if (!doc.delivery_note) {
frappe.throw(__('Please select a Delivery Note'));
} else {
let d = locals[cdt][cdn];
return {
query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details',
filters: {
delivery_note: doc.delivery_note,
}
}
}
});
frm.set_query("item_code", "items", (doc, cdt, cdn) => {
if (!doc.delivery_note) {
frappe.throw(__("Please select a Delivery Note"));
} else {
let d = locals[cdt][cdn];
return {
query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details",
filters: {
delivery_note: doc.delivery_note,
},
};
}
});
},
refresh: (frm) => {
frm.toggle_display('misc_details', frm.doc.amended_from);
frm.toggle_display("misc_details", frm.doc.amended_from);
},
delivery_note: (frm) => {
frm.set_value('items', null);
frm.set_value("items", null);
if (frm.doc.delivery_note) {
erpnext.utils.map_current_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip',
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
source_name: frm.doc.delivery_note,
target_doc: frm,
freeze: true,
freeze_message: __('Creating Packing Slip ...'),
freeze_message: __("Creating Packing Slip ..."),
});
}
},

View File

@@ -11,7 +11,7 @@ from erpnext.controllers.status_updater import StatusUpdater
class PackingSlip(StatusUpdater):
def __init__(self, *args, **kwargs) -> None:
super(PackingSlip, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.status_updater = [
{
"target_dt": "Delivery Note Item",
@@ -64,9 +64,7 @@ class PackingSlip(StatusUpdater):
"""Validate if case nos overlap. If they do, recommend next case no."""
if cint(self.from_case_no) <= 0:
frappe.throw(
_("The 'From Package No.' field must neither be empty nor it's value less than 1.")
)
frappe.throw(_("The 'From Package No.' field must neither be empty nor it's value less than 1."))
elif not self.to_case_no:
self.to_case_no = self.from_case_no
elif cint(self.to_case_no) < cint(self.from_case_no):
@@ -189,9 +187,8 @@ def item_details(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select name, item_name, description from `tabItem`
where name in ( select item_code FROM `tabDelivery Note Item`
where parent= %s)
and %s like "%s" %s
limit %s offset %s """
% ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
where parent= {})
and {} like "{}" {}
limit {} offset {} """.format("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
((filters or {}).get("delivery_note"), "%%%s%%" % txt, page_len, start),
)

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase

View File

@@ -1,61 +1,62 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Pick List', {
frappe.ui.form.on("Pick List", {
setup: (frm) => {
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; });
frm.set_indicator_formatter("item_code", function (doc) {
return doc.stock_qty === 0 ? "red" : "green";
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery Note',
'Stock Entry': 'Stock Entry',
"Delivery Note": "Delivery Note",
"Stock Entry": "Stock Entry",
};
frm.set_query('parent_warehouse', () => {
frm.set_query("parent_warehouse", () => {
return {
filters: {
'is_group': 1,
'company': frm.doc.company
}
is_group: 1,
company: frm.doc.company,
},
};
});
frm.set_query('work_order', () => {
frm.set_query("work_order", () => {
return {
query: 'erpnext.stock.doctype.pick_list.pick_list.get_pending_work_orders',
query: "erpnext.stock.doctype.pick_list.pick_list.get_pending_work_orders",
filters: {
'company': frm.doc.company
}
company: frm.doc.company,
},
};
});
frm.set_query('material_request', () => {
frm.set_query("material_request", () => {
return {
filters: {
'material_request_type': ['=', frm.doc.purpose]
}
material_request_type: ["=", frm.doc.purpose],
},
};
});
frm.set_query('item_code', 'locations', () => {
return erpnext.queries.item({ "is_stock_item": 1 });
frm.set_query("item_code", "locations", () => {
return erpnext.queries.item({ is_stock_item: 1 });
});
frm.set_query('batch_no', 'locations', (frm, cdt, cdn) => {
frm.set_query("batch_no", "locations", (frm, cdt, cdn) => {
const row = locals[cdt][cdn];
return {
query: 'erpnext.controllers.queries.get_batch_no',
query: "erpnext.controllers.queries.get_batch_no",
filters: {
item_code: row.item_code,
warehouse: row.warehouse
warehouse: row.warehouse,
},
};
});
},
set_item_locations:(frm, save) => {
set_item_locations: (frm, save) => {
if (!(frm.doc.locations && frm.doc.locations.length)) {
frappe.msgprint(__('Add items in the Item Locations table'));
frappe.msgprint(__("Add items in the Item Locations table"));
} else {
frappe.call({
method: "set_item_locations",
doc: frm.doc,
args: {
"save": save,
save: save,
},
freeze: 1,
freeze_message: __("Setting Item Locations..."),
@@ -67,155 +68,172 @@ frappe.ui.form.on('Pick List', {
frm.events.set_item_locations(frm, false);
},
refresh: (frm) => {
frm.trigger('add_get_items_button');
frm.trigger("add_get_items_button");
if (frm.doc.docstatus === 1) {
frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.target_document_exists', {
'pick_list_name': frm.doc.name,
'purpose': frm.doc.purpose
}).then(target_document_exists => {
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
frappe
.xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", {
pick_list_name: frm.doc.name,
purpose: frm.doc.purpose,
})
.then((target_document_exists) => {
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
if (target_document_exists) return;
if (target_document_exists) return;
frm.add_custom_button(__('Update Current Stock'), () => frm.trigger('update_pick_list_stock'));
frm.add_custom_button(__("Update Current Stock"), () =>
frm.trigger("update_pick_list_stock")
);
if (frm.doc.purpose === 'Delivery') {
frm.add_custom_button(__('Delivery Note'), () => frm.trigger('create_delivery_note'), __('Create'));
} else {
frm.add_custom_button(__('Stock Entry'), () => frm.trigger('create_stock_entry'), __('Create'));
}
});
if (frm.doc.purpose === "Delivery") {
frm.add_custom_button(
__("Delivery Note"),
() => frm.trigger("create_delivery_note"),
__("Create")
);
} else {
frm.add_custom_button(
__("Stock Entry"),
() => frm.trigger("create_stock_entry"),
__("Create")
);
}
});
}
},
work_order: (frm) => {
frappe.db.get_value('Work Order',
frm.doc.work_order,
['qty', 'material_transferred_for_manufacturing']
).then(data => {
let qty_data = data.message;
let max = qty_data.qty - qty_data.material_transferred_for_manufacturing;
frappe.prompt({
fieldtype: 'Float',
label: __('Qty of Finished Goods Item'),
fieldname: 'qty',
description: __('Max: {0}', [max]),
default: max
}, (data) => {
frm.set_value('for_qty', data.qty);
if (data.qty > max) {
frappe.msgprint(__('Quantity must not be more than {0}', [max]));
return;
}
frm.clear_table('locations');
erpnext.utils.map_current_doc({
method: 'erpnext.manufacturing.doctype.work_order.work_order.create_pick_list',
target: frm,
source_name: frm.doc.work_order
});
}, __('Select Quantity'), __('Get Items'));
});
frappe.db
.get_value("Work Order", frm.doc.work_order, ["qty", "material_transferred_for_manufacturing"])
.then((data) => {
let qty_data = data.message;
let max = qty_data.qty - qty_data.material_transferred_for_manufacturing;
frappe.prompt(
{
fieldtype: "Float",
label: __("Qty of Finished Goods Item"),
fieldname: "qty",
description: __("Max: {0}", [max]),
default: max,
},
(data) => {
frm.set_value("for_qty", data.qty);
if (data.qty > max) {
frappe.msgprint(__("Quantity must not be more than {0}", [max]));
return;
}
frm.clear_table("locations");
erpnext.utils.map_current_doc({
method: "erpnext.manufacturing.doctype.work_order.work_order.create_pick_list",
target: frm,
source_name: frm.doc.work_order,
});
},
__("Select Quantity"),
__("Get Items")
);
});
},
material_request: (frm) => {
erpnext.utils.map_current_doc({
method: 'erpnext.stock.doctype.material_request.material_request.create_pick_list',
method: "erpnext.stock.doctype.material_request.material_request.create_pick_list",
target: frm,
source_name: frm.doc.material_request
source_name: frm.doc.material_request,
});
},
purpose: (frm) => {
frm.clear_table('locations');
frm.trigger('add_get_items_button');
frm.clear_table("locations");
frm.trigger("add_get_items_button");
},
create_delivery_note: (frm) => {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note',
frm: frm
method: "erpnext.stock.doctype.pick_list.pick_list.create_delivery_note",
frm: frm,
});
},
create_stock_entry: (frm) => {
frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', {
'pick_list': frm.doc,
}).then(stock_entry => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", 'Stock Entry', stock_entry.name);
});
frappe
.xcall("erpnext.stock.doctype.pick_list.pick_list.create_stock_entry", {
pick_list: frm.doc,
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", "Stock Entry", stock_entry.name);
});
},
update_pick_list_stock: (frm) => {
frm.events.set_item_locations(frm, true);
},
add_get_items_button: (frm) => {
let purpose = frm.doc.purpose;
if (purpose != 'Delivery' || frm.doc.docstatus !== 0) return;
if (purpose != "Delivery" || frm.doc.docstatus !== 0) return;
let get_query_filters = {
docstatus: 1,
per_delivered: ['<', 100],
status: ['!=', ''],
customer: frm.doc.customer
per_delivered: ["<", 100],
status: ["!=", ""],
customer: frm.doc.customer,
};
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
frm.get_items_btn = frm.add_custom_button(__("Get Items"), () => {
erpnext.utils.map_current_doc({
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
source_doctype: 'Sales Order',
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
source_doctype: "Sales Order",
target: frm,
setters: {
company: frm.doc.company,
customer: frm.doc.customer
customer: frm.doc.customer,
},
date_field: 'transaction_date',
get_query_filters: get_query_filters
date_field: "transaction_date",
get_query_filters: get_query_filters,
});
});
},
scan_barcode: (frm) => {
const opts = {
frm,
items_table_name: 'locations',
qty_field: 'picked_qty',
max_qty_field: 'qty',
items_table_name: "locations",
qty_field: "picked_qty",
max_qty_field: "qty",
dont_allow_new_row: true,
prompt_qty: frm.doc.prompt_qty,
serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field.
serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field.
};
const barcode_scanner = new erpnext.utils.BarcodeScanner(opts);
barcode_scanner.process_scan();
}
},
});
frappe.ui.form.on('Pick List Item', {
frappe.ui.form.on("Pick List Item", {
item_code: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
if (row.item_code) {
get_item_details(row.item_code).then(data => {
frappe.model.set_value(cdt, cdn, 'uom', data.stock_uom);
frappe.model.set_value(cdt, cdn, 'stock_uom', data.stock_uom);
frappe.model.set_value(cdt, cdn, 'conversion_factor', 1);
get_item_details(row.item_code).then((data) => {
frappe.model.set_value(cdt, cdn, "uom", data.stock_uom);
frappe.model.set_value(cdt, cdn, "stock_uom", data.stock_uom);
frappe.model.set_value(cdt, cdn, "conversion_factor", 1);
});
}
},
uom: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
if (row.uom) {
get_item_details(row.item_code, row.uom).then(data => {
frappe.model.set_value(cdt, cdn, 'conversion_factor', data.conversion_factor);
get_item_details(row.item_code, row.uom).then((data) => {
frappe.model.set_value(cdt, cdn, "conversion_factor", data.conversion_factor);
});
}
},
qty: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor);
},
conversion_factor: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
}
frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor);
},
});
function get_item_details(item_code, uom=null) {
function get_item_details(item_code, uom = null) {
if (item_code) {
return frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.get_item_details', {
return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", {
item_code,
uom
uom,
});
}
}

View File

@@ -16,6 +16,7 @@
"for_qty",
"column_break_4",
"parent_warehouse",
"consider_rejected_warehouses",
"get_item_locations",
"section_break_6",
"scan_barcode",
@@ -184,11 +185,18 @@
"report_hide": 1,
"reqd": 1,
"search_index": 1
},
{
"default": "0",
"description": "Enable it if users want to consider rejected materials to dispatch.",
"fieldname": "consider_rejected_warehouses",
"fieldtype": "Check",
"label": "Consider Rejected Warehouses"
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-01-24 10:33:43.244476",
"modified": "2024-01-24 17:05:20.317180",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@@ -4,7 +4,6 @@
import json
from collections import OrderedDict, defaultdict
from itertools import groupby
from typing import Dict, List
import frappe
from frappe import _
@@ -32,6 +31,10 @@ class PickList(Document):
self.update_status()
self.set_item_locations()
if self.get("locations"):
self.validate_sales_order_percentage()
def validate_sales_order_percentage(self):
# set percentage picked in SO
for location in self.get("locations"):
if (
@@ -184,9 +187,9 @@ class PickList(Document):
picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict()
from_warehouses = None
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
if self.parent_warehouse:
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse))
# Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get("locations")
@@ -205,12 +208,11 @@ class PickList(Document):
self.item_count_map.get(item_code),
self.company,
picked_item_details=picked_items_details.get(item_code),
consider_rejected_warehouses=self.consider_rejected_warehouses,
),
)
locations = get_items_with_location_and_quantity(
item_doc, self.item_location_map, self.docstatus
)
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus)
item_doc.idx = None
item_doc.name = None
@@ -264,12 +266,10 @@ class PickList(Document):
item_map = OrderedDict()
for item in locations:
if not item.item_code:
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
if not cint(
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
) and not frappe.db.exists(
"Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
):
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}):
continue
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
@@ -390,7 +390,7 @@ class PickList(Document):
return picked_items
def _get_product_bundles(self) -> Dict[str, str]:
def _get_product_bundles(self) -> dict[str, str]:
# Dict[so_item_row: item_code]
product_bundles = {}
for item in self.locations:
@@ -403,13 +403,11 @@ class PickList(Document):
)
return product_bundles
def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]:
# bundle_item_code: Dict[component, qty]
product_bundle_qty_map = {}
for bundle_item_code in bundles:
bundle = frappe.get_last_doc(
"Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}
)
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0})
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
return product_bundle_qty_map
@@ -435,7 +433,7 @@ def update_pick_list_status(pick_list):
doc.run_method("update_status")
def get_picked_items_qty(items) -> List[Dict]:
def get_picked_items_qty(items) -> list[dict]:
pi_item = frappe.qb.DocType("Pick List Item")
return (
frappe.qb.from_(pi_item)
@@ -465,17 +463,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
locations = []
# if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock.
remaining_stock_qty = (
item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
)
remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
while flt(remaining_stock_qty) > 0 and available_locations:
item_location = available_locations.pop(0)
item_location = frappe._dict(item_location)
stock_qty = (
remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
)
stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
qty = stock_qty / (item_doc.conversion_factor or 1)
uom_must_be_whole_number = frappe.get_cached_value("UOM", item_doc.uom, "must_be_whole_number")
@@ -510,7 +504,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
if item_location.serial_no:
# set remaining serial numbers
item_location.serial_no = item_location.serial_no[-int(qty_diff) :]
available_locations = [item_location] + available_locations
available_locations = [item_location, *available_locations]
# update available locations for the item
item_location_map[item_doc.item_code] = available_locations
@@ -524,6 +518,7 @@ def get_available_item_locations(
company,
ignore_validation=False,
picked_item_details=None,
consider_rejected_warehouses=False,
):
locations = []
total_picked_qty = (
@@ -534,19 +529,39 @@ def get_available_item_locations(
if has_batch_no and has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_batch_no:
locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
else:
locations = get_available_item_locations_for_other_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty,
consider_rejected_warehouses=consider_rejected_warehouses,
)
total_qty_available = sum(location.get("qty") for location in locations)
@@ -597,7 +612,12 @@ def get_available_item_locations(
def get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty=0,
consider_rejected_warehouses=False,
):
sn = frappe.qb.DocType("Serial No")
query = (
@@ -613,6 +633,10 @@ def get_available_item_locations_for_serialized_item(
else:
query = query.where(Coalesce(sn.warehouse, "") != "")
if not consider_rejected_warehouses:
if rejected_warehouses := get_rejected_warehouses():
query = query.where(sn.warehouse.notin(rejected_warehouses))
serial_nos = query.run(as_list=True)
warehouse_serial_nos_map = frappe._dict()
@@ -627,7 +651,12 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty=0,
consider_rejected_warehouses=False,
):
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
@@ -653,15 +682,28 @@ def get_available_item_locations_for_batched_item(
if from_warehouses:
query = query.where(sle.warehouse.isin(from_warehouses))
if not consider_rejected_warehouses:
if rejected_warehouses := get_rejected_warehouses():
query = query.where(sle.warehouse.notin(rejected_warehouses))
return query.run(as_dict=True)
def get_available_item_locations_for_serial_and_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty=0,
consider_rejected_warehouses=False,
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company
item_code,
from_warehouses,
required_qty,
company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
if locations:
@@ -691,7 +733,12 @@ def get_available_item_locations_for_serial_and_batched_item(
def get_available_item_locations_for_other_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
item_code,
from_warehouses,
required_qty,
company,
total_picked_qty=0,
consider_rejected_warehouses=False,
):
bin = frappe.qb.DocType("Bin")
query = (
@@ -708,6 +755,10 @@ def get_available_item_locations_for_other_item(
wh = frappe.qb.DocType("Warehouse")
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
if not consider_rejected_warehouses:
if rejected_warehouses := get_rejected_warehouses():
query = query.where(bin.warehouse.notin(rejected_warehouses))
item_locations = query.run(as_dict=True)
return item_locations
@@ -768,8 +819,7 @@ def create_dn_with_so(sales_dict, pick_list):
"name": "so_detail",
"parent": "against_sales_order",
},
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1,
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
}
for customer in sales_dict:
@@ -790,7 +840,6 @@ def create_dn_with_so(sales_dict, pick_list):
def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
for location in pick_list.locations:
if location.sales_order != sales_order or location.product_bundle_item:
continue
@@ -821,9 +870,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
def add_product_bundles_to_delivery_note(
pick_list: "PickList", delivery_note, item_mapper
) -> None:
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
"""Add product bundles found in pick list to delivery note.
When mapping pick list items, the bundle item itself isn't part of the
@@ -901,7 +948,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
& (wo.qty > wo.material_transferred_for_manufacturing)
& (wo.docstatus == 1)
& (wo.company == filters.get("company"))
& (wo.name.like("%{0}%".format(txt)))
& (wo.name.like(f"%{txt}%"))
)
.orderby(Case().when(Locate(txt, wo.name) > 0, Locate(txt, wo.name)).else_(99999))
.orderby(wo.name)
@@ -968,9 +1015,7 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
stock_entry.fg_completed_qty = pick_list.for_qty
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value(
"BOM", work_order.bom_no, "inspection_required"
)
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
if not (is_wip_warehouse_group and work_order.skip_transfer):
@@ -1028,3 +1073,15 @@ def update_common_item_properties(item, location):
item.serial_no = location.serial_no
item.batch_no = location.batch_no
item.material_request_item = location.material_request_item
def get_rejected_warehouses():
if not hasattr(frappe.local, "rejected_warehouses"):
frappe.local.rejected_warehouses = []
if not frappe.local.rejected_warehouses:
frappe.local.rejected_warehouses = frappe.get_all(
"Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name"
)
return frappe.local.rejected_warehouses

View File

@@ -1,14 +1,14 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings['Pick List'] = {
frappe.listview_settings["Pick List"] = {
get_indicator: function (doc) {
const status_colors = {
"Draft": "grey",
"Open": "orange",
"Completed": "green",
"Cancelled": "red",
Draft: "grey",
Open: "orange",
Completed: "green",
Cancelled: "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};
};

View File

@@ -139,7 +139,6 @@ class TestPickList(FrappeTestCase):
self.assertEqual(pick_list.locations[1].qty, 10)
def test_pick_list_shows_serial_no_for_serialized_item(self):
stock_reconciliation = frappe.get_doc(
{
"doctype": "Stock Reconciliation",
@@ -274,7 +273,6 @@ class TestPickList(FrappeTestCase):
pr2.cancel()
def test_pick_list_for_items_from_multiple_sales_orders(self):
item_code = make_item().name
try:
frappe.get_doc(
@@ -418,9 +416,7 @@ class TestPickList(FrappeTestCase):
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
self.assertEqual(
sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor
)
self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
pick_list.cancel()
sales_order.cancel()
@@ -469,7 +465,7 @@ class TestPickList(FrappeTestCase):
_dict(item_code="A", warehouse="X", qty=8, picked_qty=3),
_dict(item_code="B", warehouse="Y", qty=6, picked_qty=4),
]
for expected_item, created_item in zip(expected_items, pl.locations):
for expected_item, created_item in zip(expected_items, pl.locations, strict=False):
_compare_dicts(expected_item, created_item)
def test_multiple_dn_creation(self):
@@ -579,9 +575,7 @@ class TestPickList(FrappeTestCase):
pick_list_1.set_item_locations()
pick_list_1.submit()
create_delivery_note(pick_list_1.name)
for dn in frappe.get_all(
"Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}
):
for dn in frappe.get_all("Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
if dn_item.item_code == "_Test Item":
self.assertEqual(dn_item.qty, 1)
@@ -609,7 +603,7 @@ class TestPickList(FrappeTestCase):
quantities = [5, 2]
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
bundle_items = dict(zip(components, quantities))
bundle_items = dict(zip(components, quantities, strict=False))
so = make_sales_order(item_code=bundle, qty=3, rate=42)
@@ -715,7 +709,7 @@ class TestPickList(FrappeTestCase):
for item in items:
for warehouse in warehouses:
se = make_stock_entry(
make_stock_entry(
item=item.get("item_code"),
to_warehouse=warehouse,
qty=5,

View File

@@ -2,13 +2,17 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Price List", {
refresh: function(frm) {
refresh: function (frm) {
let me = this;
frm.add_custom_button(__("Add / Edit Prices"), function() {
frappe.route_options = {
"price_list": frm.doc.name
};
frappe.set_route("Report", "Item Price");
}, "fa fa-money");
}
frm.add_custom_button(
__("Add / Edit Prices"),
function () {
frappe.route_options = {
price_list: frm.doc.name,
};
frappe.set_route("Report", "Item Price");
},
"fa fa-money"
);
},
});

View File

@@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", {
}, __('Create'));
}
if (frm.doc.docstatus === 0) {
if (!frm.doc.is_return) {
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
if (value) {
frm.doc.items.forEach((item) => {
frm.fields_dict.items.grid.update_docfield_property(
"rate", "read_only", (item.purchase_order && item.purchase_order_item)
);
});
}
});
}
}
frm.events.add_custom_buttons(frm);
},

View File

@@ -649,7 +649,7 @@
},
{
"fieldname": "other_charges_calculation",
"fieldtype": "Long Text",
"fieldtype": "Text Editor",
"label": "Taxes and Charges Calculation",
"no_copy": 1,
"oldfieldtype": "HTML",
@@ -1242,7 +1242,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2023-12-18 17:26:41.279663",
"modified": "2024-03-20 16:05:31.713453",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -14,6 +14,7 @@ import erpnext
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
@@ -22,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
class PurchaseReceipt(BuyingController):
def __init__(self, *args, **kwargs):
super(PurchaseReceipt, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.status_updater = [
{
"target_dt": "Purchase Order Item",
@@ -115,7 +116,7 @@ class PurchaseReceipt(BuyingController):
def validate(self):
self.validate_posting_time()
super(PurchaseReceipt, self).validate()
super().validate()
if self._action == "submit":
self.make_batches("warehouse")
@@ -125,8 +126,7 @@ class PurchaseReceipt(BuyingController):
self.po_required()
self.validate_items_quality_inspection()
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer()
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
@@ -140,6 +140,10 @@ class PurchaseReceipt(BuyingController):
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_uom_is_integer(self):
super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item")
super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item")
def validate_cwip_accounts(self):
for item in self.get("items"):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
@@ -147,15 +151,15 @@ class PurchaseReceipt(BuyingController):
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
self.get_company_default("asset_received_but_not_billed")
get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
"capital_work_in_progress_account",
asset_category=item.asset_category,
company=self.company,
)
break
def validate_provisional_expense_account(self):
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
)
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
)
if not provisional_accounting_for_non_stock_items:
@@ -167,7 +171,7 @@ class PurchaseReceipt(BuyingController):
item.provisional_expense_account = default_provisional_account
def validate_with_previous_doc(self):
super(PurchaseReceipt, self).validate_with_previous_doc(
super().validate_with_previous_doc(
{
"Purchase Order": {
"ref_dn_field": "purchase_order",
@@ -228,24 +232,20 @@ class PurchaseReceipt(BuyingController):
return qty and flt(qty[0][0]) or 0.0
def get_po_qty_and_warehouse(self, po_detail):
po_qty, po_warehouse = frappe.db.get_value(
"Purchase Order Item", po_detail, ["qty", "warehouse"]
)
po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"])
return po_qty, po_warehouse
# Check for Closed status
def check_on_hold_or_closed_status(self):
check_list = []
for d in self.get("items"):
if (
d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list
):
if d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list:
check_list.append(d.purchase_order)
check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
# on submit
def on_submit(self):
super(PurchaseReceipt, self).on_submit()
super().on_submit()
# Check for Approving Authority
frappe.get_doc("Authorization Control").validate_approving_authority(
@@ -282,7 +282,7 @@ class PurchaseReceipt(BuyingController):
frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0]))
def on_cancel(self):
super(PurchaseReceipt, self).on_cancel()
super().on_cancel()
self.check_on_hold_or_closed_status()
# Check if Purchase Invoice has been submitted against current Purchase Order
@@ -324,9 +324,7 @@ class PurchaseReceipt(BuyingController):
)
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
)
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
)
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
@@ -395,7 +393,6 @@ class PurchaseReceipt(BuyingController):
and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
and item.net_rate == net_rate_map[item.purchase_invoice_item]
):
discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
exchange_rate_map[item.purchase_invoice] - self.conversion_rate
)
@@ -572,17 +569,17 @@ class PurchaseReceipt(BuyingController):
)
stock_value_diff = (
flt(d.base_net_amount)
+ flt(d.item_tax_amount / self.conversion_rate)
+ flt(d.landed_cost_voucher_amount)
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
)
elif warehouse_account.get(d.warehouse):
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
stock_asset_account_name = warehouse_account[d.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
"account_currency"
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
"account"
)
supplier_warehouse_account_currency = warehouse_account.get(
self.supplier_warehouse, {}
).get("account_currency")
# If PR is sub-contracted and fg item rate is zero
# in that case if account for source and target warehouse are same,
@@ -606,7 +603,7 @@ class PurchaseReceipt(BuyingController):
):
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset:
if d.is_fixed_asset and d.landed_cost_voucher_amount:
self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account:
@@ -617,16 +614,19 @@ class PurchaseReceipt(BuyingController):
)
def add_provisional_gl_entry(
self, item, gl_entries, posting_date, provisional_account, reverse=0
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
):
credit_currency = get_account_currency(provisional_account)
expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1
amount = item.base_amount
if reverse:
multiplication_factor = -1
# Post reverse entry for previously posted amount
amount = item_amount
expense_account = frappe.db.get_value(
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
)
@@ -636,7 +636,7 @@ class PurchaseReceipt(BuyingController):
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * item.base_amount,
credit=multiplication_factor * amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
@@ -650,7 +650,7 @@ class PurchaseReceipt(BuyingController):
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
debit=multiplication_factor * item.base_amount,
debit=multiplication_factor * amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,
@@ -738,11 +738,14 @@ class PurchaseReceipt(BuyingController):
)
for asset in assets:
purchase_amount = flt(valuation_rate) * asset.asset_quantity
frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity
)
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity
"Asset",
asset.name,
{
"gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
)
def update_status(self, status):
@@ -971,7 +974,7 @@ def get_item_wise_returned_qty(pr_doc):
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
def make_purchase_invoice(source_name, target_doc=None, args=None):
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc("Purchase Receipt", source_name)
@@ -983,19 +986,19 @@ def make_purchase_invoice(source_name, target_doc=None):
frappe.throw(_("All items have already been Invoiced/Returned"))
doc = frappe.get_doc(target)
doc.payment_terms_template = get_payment_terms_template(
source.supplier, "Supplier", source.company
)
doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
doc.run_method("onload")
doc.run_method("set_missing_values")
if args and args.get("merge_taxes"):
merge_taxes(source.get("taxes") or [], doc)
doc.run_method("calculate_taxes_and_totals")
doc.set_payment_schedule()
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
if frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
):
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
target_doc.rejected_qty = 0
target_doc.stock_qty = flt(target_doc.qty) * flt(
target_doc.conversion_factor, target_doc.precision("conversion_factor")
@@ -1004,9 +1007,7 @@ def make_purchase_invoice(source_name, target_doc=None):
def get_pending_qty(item_row):
qty = item_row.qty
if frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
):
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
qty = item_row.received_qty
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
@@ -1039,6 +1040,7 @@ def make_purchase_invoice(source_name, target_doc=None):
"field_map": {
"name": "pr_detail",
"parent": "purchase_receipt",
"qty": "received_qty",
"purchase_order_item": "po_detail",
"purchase_order": "purchase_order",
"is_fixed_asset": "is_fixed_asset",
@@ -1051,7 +1053,11 @@ def make_purchase_invoice(source_name, target_doc=None):
if not doc.get("is_return")
else get_pending_qty(d)[0] > 0,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"add_if_empty": True,
"ignore": args.get("merge_taxes") if args else 0,
},
},
target_doc,
set_missing_values,
@@ -1157,16 +1163,16 @@ def get_item_account_wise_additional_cost(purchase_document):
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
based_on_field = None
# 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:
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
total_item_cost = 0
for item in landed_cost_voucher_doc.items:
total_item_cost += item.get(based_on_field)
if based_on_field:
for item in landed_cost_voucher_doc.items:
total_item_cost += item.get(based_on_field)
for item in landed_cost_voucher_doc.items:
if item.receipt_document == purchase_document:
@@ -1179,15 +1185,11 @@ def get_item_account_wise_additional_cost(purchase_document):
if total_item_cost > 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
)
]["amount"] += account.amount * item.get(based_on_field) / total_item_cost
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
]["base_amount"] += account.base_amount * item.get(based_on_field) / total_item_cost
else:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account

View File

@@ -1,8 +1,17 @@
frappe.listview_settings['Purchase Receipt'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "is_subcontracted",
"transporter_name", "is_return", "status", "per_billed", "currency"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
frappe.listview_settings["Purchase Receipt"] = {
add_fields: [
"supplier",
"supplier_name",
"base_grand_total",
"is_subcontracted",
"transporter_name",
"is_return",
"status",
"per_billed",
"currency",
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1) {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
@@ -15,11 +24,9 @@ frappe.listview_settings['Purchase Receipt'] = {
}
},
onload: function(listview) {
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
onload: function (listview) {
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice");
});
}
},
};

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
from frappe.utils import add_days, cint, cstr, flt, nowtime, today
from pypika import functions as fn
import erpnext
@@ -11,9 +11,8 @@ from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
class TestPurchaseReceipt(FrappeTestCase):
@@ -38,7 +37,6 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.delete()
def test_reverse_purchase_receipt_sle(self):
pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200")
sl_entry = frappe.db.get_all(
@@ -125,9 +123,7 @@ class TestPurchaseReceipt(FrappeTestCase):
pi.delete() # draft PI
pr.cancel()
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
frappe.get_doc(
"Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
).delete()
frappe.get_doc("Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice").delete()
def test_purchase_receipt_no_gl_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -194,87 +190,8 @@ class TestPurchaseReceipt(FrappeTestCase):
batch_no = pr.items[0].batch_no
pr.cancel()
self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
def test_duplicate_serial_nos(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"})
if not item:
item = create_item("Test Serialized Item 123")
item.has_serial_no = 1
item.serial_no_series = "TSI123-.####"
item.save()
else:
item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"})
# First make purchase receipt
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
pr.load_from_db()
serial_nos = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name},
"serial_no",
)
serial_nos = get_serial_nos(serial_nos)
self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos)
# Then tried to receive same serial nos in difference company
pr_different_company = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
serial_no="\n".join(serial_nos),
company="_Test Company 1",
do_not_submit=True,
warehouse="Stores - _TC1",
)
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
# Then made delivery note to remove the serial nos from stock
dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos))
dn.load_from_db()
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
posting_date = add_days(today(), -3)
# Try to receive same serial nos again in the same company with backdated.
pr1 = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
posting_date=posting_date,
serial_no="\n".join(serial_nos),
do_not_submit=True,
)
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
# Try to receive same serial nos with different company with backdated.
pr2 = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
posting_date=posting_date,
serial_no="\n".join(serial_nos),
company="_Test Company 1",
do_not_submit=True,
warehouse="Stores - _TC1",
)
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
# Receive the same serial nos after the delivery note posting date and time
make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos))
# Raise the error for backdated deliver note entry cancel
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
def test_purchase_receipt_gl_entry(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
@@ -353,7 +270,8 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(len(rejected_serial_nos), 2)
for serial_no in rejected_serial_nos:
self.assertEqual(
frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse
frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse,
)
pr.cancel()
@@ -693,7 +611,7 @@ class TestPurchaseReceipt(FrappeTestCase):
item_code = "Test Manual Created Serial No"
if not frappe.db.exists("Item", item_code):
item = make_item(item_code, dict(has_serial_no=1))
make_item(item_code, dict(has_serial_no=1))
serial_no = "12903812901"
pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
@@ -839,7 +757,7 @@ class TestPurchaseReceipt(FrappeTestCase):
"Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
@@ -864,7 +782,7 @@ class TestPurchaseReceipt(FrappeTestCase):
"Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
for _i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
@@ -1125,9 +1043,7 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value(
"Company", pr.company, "exchange_gain_loss_account"
)
exchange_gain_loss_account = frappe.db.get_value("Company", pr.company, "exchange_gain_loss_account")
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
@@ -1184,9 +1100,7 @@ class TestPurchaseReceipt(FrappeTestCase):
account = "Stock Received But Not Billed - TCP1"
make_item(item_code)
se = make_stock_entry(
item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0
)
se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
se.items[0].allow_zero_valuation_rate = 1
se.save()
se.submit()
@@ -1301,9 +1215,7 @@ class TestPurchaseReceipt(FrappeTestCase):
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
rejected_warehouse = create_warehouse(
"_Test Rejected Internal To Warehouse New", company=company
)
rejected_warehouse = create_warehouse("_Test Rejected Internal To Warehouse New", company=company)
item_doc = make_item(
"Test Internal Transfer Item DS",
{
@@ -1674,9 +1586,10 @@ class TestPurchaseReceipt(FrappeTestCase):
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=15,
qty=20,
company=company,
to_warehouse=from_warehouse,
posting_date=add_days(today(), -3),
)
# Step 3: Create Delivery Note with Internal Customer
@@ -1695,17 +1608,18 @@ class TestPurchaseReceipt(FrappeTestCase):
)
# Step 4: Create Internal Purchase Receipt
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.set_posting_time = 1
pr.posting_date = today()
pr.items[0].qty = 15
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
self.assertRaises(OverAllowanceError, pr.submit)
self.assertRaises(frappe.ValidationError, pr.submit)
# Step 5: Test Over Receipt Allowance
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
@@ -1717,8 +1631,10 @@ class TestPurchaseReceipt(FrappeTestCase):
company=company,
from_warehouse=from_warehouse,
to_warehouse=target_warehouse,
posting_date=add_days(pr.posting_date, -1),
)
pr.reload()
pr.submit()
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
@@ -1841,7 +1757,6 @@ class TestPurchaseReceipt(FrappeTestCase):
)
# Step 4: Create Internal Purchase Receipt
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
@@ -2147,9 +2062,7 @@ class TestPurchaseReceipt(FrappeTestCase):
)
# Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched])
make_stock_entry(
purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100
)
make_stock_entry(purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100)
# Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched])
return_pr = make_purchase_receipt(
@@ -2221,9 +2134,9 @@ class TestPurchaseReceipt(FrappeTestCase):
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)
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)
@@ -2251,6 +2164,143 @@ class TestPurchaseReceipt(FrappeTestCase):
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
def test_do_not_delete_batch_implicitly(self):
item = make_item(
"_Test Item With Delete Batch",
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TBWDB.#####"},
).name
pr = make_purchase_receipt(item_code=item, qty=10, rate=100)
batch_no = pr.items[0].batch_no
self.assertTrue(frappe.db.exists("Batch", batch_no))
pr.cancel()
self.assertTrue(frappe.db.exists("Batch", batch_no))
def test_pr_billed_amount_against_return_entry(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
# Create a Purchase Receipt and Fully Bill it
pr = make_purchase_receipt(qty=10)
pi = make_pi_from_pr(pr.name)
pi.insert()
pi.submit()
# Debit Note - 50% Qty & enable updating PR billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_receipt = 1
pi_return.submit()
# Check if the billed amount reduced
pr.reload()
self.assertEqual(pr.per_billed, 50)
pi_return.reload()
pi_return.cancel()
# Debit Note - 50% Qty & disable updating PR billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_receipt = 0
pi_return.submit()
# Check if the billed amount stayed the same
pr.reload()
self.assertEqual(pr.per_billed, 100)
def test_sle_qty_after_transaction(self):
item = make_item(
"_Test Item Qty After Transaction",
properties={"is_stock_item": 1, "valuation_method": "FIFO"},
).name
posting_date = today()
posting_time = nowtime()
# Step 1: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for _i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 1 + index)
# Step 2: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for _i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 11 + index)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -2308,7 +2358,6 @@ def get_gl_entries(voucher_type, voucher_no):
def get_taxes(**args):
args = frappe._dict(args)
return [
@@ -2428,15 +2477,14 @@ def make_purchase_receipt(**args):
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC"
if rejected_qty != 0
else "",
"rate": args.rate if args.rate != None else 50,
"rate": args.rate if args.rate is not None else 50,
"conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
"batch_no": args.batch_no,
"stock_uom": args.stock_uom or "_Test UOM",
"uom": uom,
"cost_center": args.cost_center
or frappe.get_cached_value("Company", pr.company, "cost_center"),
"cost_center": args.cost_center or frappe.get_cached_value("Company", pr.company, "cost_center"),
"asset_location": args.location or "Test Location",
},
)

View File

@@ -352,7 +352,6 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_width": "100px",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
"width": "100px"
},
{
@@ -1055,7 +1054,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-30 16:12:02.364608",
"modified": "2023-12-25 22:32:09.801965",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -1,41 +1,41 @@
// 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() {
frappe.ui.form.on("Putaway Rule", {
setup: function (frm) {
frm.set_query("warehouse", function () {
return {
"filters": {
"company": frm.doc.company,
"is_group": 0
}
filters: {
company: frm.doc.company,
is_group: 0,
},
};
});
},
uom: function(frm) {
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
uom: frm.doc.uom,
},
callback: function(r) {
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);
frm.set_value("conversion_factor", r.message.conversion_factor);
frm.set_value("stock_capacity", stock_capacity);
}
}
},
});
}
},
capacity: function(frm) {
capacity: function (frm) {
let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor);
frm.set_value('stock_capacity', stock_capacity);
}
frm.set_value("stock_capacity", stock_capacity);
},
// refresh: function(frm) {

View File

@@ -109,9 +109,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
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
)
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
if not rules:
warehouse = source_warehouse or item.get("warehouse")
@@ -204,7 +202,7 @@ def _items_changed(old, new, doctype: str) -> bool:
new_sorted = sorted(new, key=sort_key)
# Once sorted by all relevant keys both tables should align if they are same.
for old_item, new_item in zip(old_sorted, new_sorted):
for old_item, new_item in zip(old_sorted, new_sorted, strict=False):
for key in compare_keys:
if old_item.get(key) != new_item.get(key):
return True
@@ -253,9 +251,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
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
)
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
@@ -277,24 +273,20 @@ def show_unassigned_items_message(items_not_accomodated):
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])
)
formatted_item_rows += f"""
<td>{item_link}</td>
<td>{frappe.bold(entry[1])}</td>
</tr>"""
msg += """
<table class="table">
<thead>
<td>{0}</td>
<td>{1}</td>
<td>{}</td>
<td>{}</td>
</thead>
{2}
{}
</table>
""".format(
_("Item"), _("Unassigned Qty"), formatted_item_rows
)
""".format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)

View File

@@ -1,4 +1,4 @@
frappe.listview_settings['Putaway Rule'] = {
frappe.listview_settings["Putaway Rule"] = {
add_fields: ["disable"],
get_indicator: (doc) => {
if (doc.disable) {
@@ -10,9 +10,9 @@ frappe.listview_settings['Putaway Rule'] = {
reports: [
{
name: 'Warehouse Capacity Summary',
report_type: 'Page',
route: 'warehouse-capacity-summary'
}
]
name: "Warehouse Capacity Summary",
report_type: "Page",
route: "warehouse-capacity-summary",
},
],
};

View File

@@ -46,9 +46,7 @@ class TestPutawayRule(FrappeTestCase):
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_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
)
@@ -69,17 +67,11 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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
)
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)
@@ -97,12 +89,8 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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)
@@ -123,19 +111,13 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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"
)
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
)
stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50)
pr = make_purchase_receipt(
item_code="_Rice",
@@ -167,9 +149,7 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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)
@@ -199,9 +179,7 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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)
@@ -237,9 +215,7 @@ class TestPutawayRule(FrappeTestCase):
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"
)
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)
@@ -291,9 +267,7 @@ class TestPutawayRule(FrappeTestCase):
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_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
)
@@ -428,9 +402,7 @@ class TestPutawayRule(FrappeTestCase):
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"
)
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg")
stock_entry = make_stock_entry(
item_code="_Rice",
@@ -480,9 +452,7 @@ def create_putaway_rule(**args):
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"
]
putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)["conversion_factor"]
if not args.do_not_save:
putaway.save()

View File

@@ -4,88 +4,85 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", {
setup: function(frm) {
frm.set_query("reference_name", function() {
setup: function (frm) {
frm.set_query("reference_name", function () {
return {
filters: {
"docstatus": ["!=", 2],
}
}
docstatus: ["!=", 2],
},
};
});
frm.set_query("batch_no", function() {
frm.set_query("batch_no", function () {
return {
filters: {
"item": frm.doc.item_code
}
item: frm.doc.item_code,
},
};
});
// Serial No based on item_code
frm.set_query("item_serial_no", function() {
frm.set_query("item_serial_no", function () {
let filters = {};
if (frm.doc.item_code) {
filters = {
'item_code': frm.doc.item_code
item_code: frm.doc.item_code,
};
}
return { filters: filters };
});
// item code based on GRN/DN
frm.set_query("item_code", function(doc) {
frm.set_query("item_code", function (doc) {
let doctype = doc.reference_type;
if (doc.reference_type !== "Job Card") {
doctype = (doc.reference_type == "Stock Entry") ?
"Stock Entry Detail" : doc.reference_type + " Item";
doctype =
doc.reference_type == "Stock Entry" ? "Stock Entry Detail" : doc.reference_type + " Item";
}
if (doc.reference_type && doc.reference_name) {
let filters = {
"from": doctype,
"inspection_type": doc.inspection_type
from: doctype,
inspection_type: doc.inspection_type,
};
if (doc.reference_type == doctype)
filters["reference_name"] = doc.reference_name;
else
filters["parent"] = doc.reference_name;
if (doc.reference_type == doctype) filters["reference_name"] = doc.reference_name;
else filters["parent"] = doc.reference_name;
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
filters: filters
filters: filters,
};
}
});
},
refresh: function(frm) {
refresh: function (frm) {
// Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
},
item_code: function(frm) {
item_code: function (frm) {
if (frm.doc.item_code && !frm.doc.quality_inspection_template) {
return frm.call({
method: "get_quality_inspection_template",
doc: frm.doc,
callback: function() {
refresh_field(['quality_inspection_template', 'readings']);
}
callback: function () {
refresh_field(["quality_inspection_template", "readings"]);
},
});
}
},
quality_inspection_template: function(frm) {
quality_inspection_template: function (frm) {
if (frm.doc.quality_inspection_template) {
return frm.call({
method: "get_item_specification_details",
doc: frm.doc,
callback: function() {
refresh_field('readings');
}
callback: function () {
refresh_field("readings");
},
});
}
},

View File

@@ -68,6 +68,9 @@ class QualityInspection(Document):
def on_cancel(self):
self.update_qc_reference()
def on_trash(self):
self.update_qc_reference()
def validate_readings_status_mandatory(self):
for reading in self.readings:
if not reading.status:
@@ -79,13 +82,11 @@ class QualityInspection(Document):
if self.reference_type == "Job Card":
if self.reference_name:
frappe.db.sql(
"""
UPDATE `tab{doctype}`
f"""
UPDATE `tab{self.reference_type}`
SET quality_inspection = %s, modified = %s
WHERE name = %s and production_item = %s
""".format(
doctype=self.reference_type
),
""",
(quality_inspection, self.modified, self.reference_name, self.item_code),
)
@@ -107,9 +108,9 @@ class QualityInspection(Document):
args.append(self.name)
frappe.db.sql(
"""
f"""
UPDATE
`tab{child_doc}` t1, `tab{parent_doc}` t2
`tab{doctype}` t1, `tab{self.reference_type}` t2
SET
t1.quality_inspection = %s, t2.modified = %s
WHERE
@@ -117,9 +118,7 @@ class QualityInspection(Document):
and t1.item_code = %s
and t1.parent = t2.name
{conditions}
""".format(
parent_doc=self.reference_type, child_doc=doctype, conditions=conditions
),
""",
args,
)
@@ -177,9 +176,9 @@ class QualityInspection(Document):
except NameError as e:
field = frappe.bold(e.args[0].split()[1])
frappe.throw(
_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.").format(
reading.idx, field
),
_(
"Row #{0}: {1} is not a valid reading field. Please refer to the field description."
).format(reading.idx, field),
title=_("Invalid Formula"),
)
except Exception:
@@ -248,40 +247,26 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
qi_condition = ""
return frappe.db.sql(
"""
f"""
SELECT item_code
FROM `tab{doc}`
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY item_code limit {page_len} offset {start}
""".format(
doc=from_doctype,
cond=cond,
mcond=mcond,
start=cint(start),
page_len=cint(page_len),
qi_condition=qi_condition,
),
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
""",
{"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
)
elif filters.get("reference_name"):
return frappe.db.sql(
"""
f"""
SELECT production_item
FROM `tab{doc}`
FROM `tab{from_doctype}`
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY production_item
limit {page_len} offset {start}
""".format(
doc=from_doctype,
cond=cond,
mcond=mcond,
start=cint(start),
page_len=cint(page_len),
qi_condition=qi_condition,
),
limit {cint(page_len)} offset {cint(start)}
""",
{"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
)

View File

@@ -159,9 +159,7 @@ class TestQualityInspection(FrappeTestCase):
do_not_submit=True,
)
readings = [
{"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}
]
readings = [{"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}]
qa = create_quality_inspection(
reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected"
@@ -216,6 +214,33 @@ class TestQualityInspection(FrappeTestCase):
qa.save()
self.assertEqual(qa.status, "Accepted")
def test_delete_quality_inspection_linked_with_stock_entry(self):
item_code = create_item("_Test Cicuular Dependecy Item with QA").name
se = make_stock_entry(
item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True
)
se.inspection_required = 1
se.save()
qa = create_quality_inspection(
item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True
)
se.reload()
se.items[0].quality_inspection = qa.name
se.save()
qa.delete()
se.reload()
qc = se.items[0].quality_inspection
self.assertFalse(qc)
se.delete()
def create_quality_inspection(**args):
args = frappe._dict(args)

View File

@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Quality Inspection Parameter', {
frappe.ui.form.on("Quality Inspection Parameter", {
// refresh: function(frm) {
// }
});

View File

@@ -1,8 +1,7 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Quality Inspection Parameter Group', {
frappe.ui.form.on("Quality Inspection Parameter Group", {
// refresh: function(frm) {
// }
});

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Quality Inspection Template', {
refresh: function() {
}
frappe.ui.form.on("Quality Inspection Template", {
refresh: function () {},
});

View File

@@ -1,91 +1,90 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Quick Stock Balance', {
frappe.ui.form.on("Quick Stock Balance", {
setup: (frm) => {
frm.set_query('item', () => {
frm.set_query("item", () => {
if (!(frm.doc.warehouse && frm.doc.date)) {
frm.trigger('check_warehouse_and_date');
frm.trigger("check_warehouse_and_date");
}
});
},
make_custom_stock_report_button: (frm) => {
if (frm.doc.item) {
frm.add_custom_button(__('Stock Balance Report'), () => {
frappe.set_route('query-report', 'Stock Balance',
{ 'item_code': frm.doc.item, 'warehouse': frm.doc.warehouse });
frm.add_custom_button(__("Stock Balance Report"), () => {
frappe.set_route("query-report", "Stock Balance", {
item_code: frm.doc.item,
warehouse: frm.doc.warehouse,
});
});
}
},
refresh: (frm) => {
frm.disable_save();
frm.trigger('make_custom_stock_report_button');
frm.trigger("make_custom_stock_report_button");
},
check_warehouse_and_date: (frm) => {
frappe.msgprint(__('Please enter Warehouse and Date'));
frm.doc.item = '';
frappe.msgprint(__("Please enter Warehouse and Date"));
frm.doc.item = "";
frm.refresh();
},
warehouse: (frm) => {
if (frm.doc.item || frm.doc.item_barcode) {
frm.trigger('get_stock_and_item_details');
frm.trigger("get_stock_and_item_details");
}
},
date: (frm) => {
if (frm.doc.item || frm.doc.item_barcode) {
frm.trigger('get_stock_and_item_details');
frm.trigger("get_stock_and_item_details");
}
},
item: (frm) => {
frappe.flags.last_updated_element = 'item';
frm.trigger('get_stock_and_item_details');
frm.trigger('make_custom_stock_report_button');
frappe.flags.last_updated_element = "item";
frm.trigger("get_stock_and_item_details");
frm.trigger("make_custom_stock_report_button");
},
item_barcode: (frm) => {
frappe.flags.last_updated_element = 'item_barcode';
frm.trigger('get_stock_and_item_details');
frm.trigger('make_custom_stock_report_button');
frappe.flags.last_updated_element = "item_barcode";
frm.trigger("get_stock_and_item_details");
frm.trigger("make_custom_stock_report_button");
},
get_stock_and_item_details: (frm) => {
if (!(frm.doc.warehouse && frm.doc.date)) {
frm.trigger('check_warehouse_and_date');
}
else if (frm.doc.item || frm.doc.item_barcode) {
frm.trigger("check_warehouse_and_date");
} else if (frm.doc.item || frm.doc.item_barcode) {
let filters = {
warehouse: frm.doc.warehouse,
date: frm.doc.date,
};
if (frappe.flags.last_updated_element === 'item') {
filters = { ...filters, ...{ item: frm.doc.item }};
}
else {
filters = { ...filters, ...{ barcode: frm.doc.item_barcode }};
if (frappe.flags.last_updated_element === "item") {
filters = { ...filters, ...{ item: frm.doc.item } };
} else {
filters = { ...filters, ...{ barcode: frm.doc.item_barcode } };
}
frappe.call({
method: 'erpnext.stock.doctype.quick_stock_balance.quick_stock_balance.get_stock_item_details',
method: "erpnext.stock.doctype.quick_stock_balance.quick_stock_balance.get_stock_item_details",
args: filters,
callback: (r) => {
if (r.message) {
let fields = ['item', 'qty', 'value', 'image'];
if (!r.message['barcodes'].includes(frm.doc.item_barcode)) {
frm.doc.item_barcode = '';
let fields = ["item", "qty", "value", "image"];
if (!r.message["barcodes"].includes(frm.doc.item_barcode)) {
frm.doc.item_barcode = "";
frm.refresh();
}
fields.forEach(function (field) {
frm.set_value(field, r.message[field]);
});
}
}
},
});
}
}
},
});

View File

@@ -17,17 +17,13 @@ class QuickStockBalance(Document):
def get_stock_item_details(warehouse, date, item=None, barcode=None):
out = {}
if barcode:
out["item"] = frappe.db.get_value(
"Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]
)
out["item"] = frappe.db.get_value("Item Barcode", filters={"barcode": barcode}, fieldname=["parent"])
if not out["item"]:
frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode."))
else:
out["item"] = item
barcodes = frappe.db.get_values(
"Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"]
)
barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"])
out["barcodes"] = [x[0] for x in barcodes]
out["qty"] = get_stock_balance(out["item"], warehouse, date)

View File

@@ -1,22 +1,32 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Repost Item Valuation', {
setup: function(frm) {
frappe.ui.form.on("Repost Item Valuation", {
setup: function (frm) {
frm.set_query("warehouse", () => {
let filters = {
'is_group': 0
is_group: 0,
};
if (frm.doc.company) filters['company'] = frm.doc.company;
return {filters: filters};
if (frm.doc.company) filters["company"] = frm.doc.company;
return { filters: filters };
});
frm.set_query("voucher_type", () => {
return {
filters: {
name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note',
'Sales Invoice', 'Stock Entry', 'Stock Reconciliation', 'Subcontracting Receipt']]
}
name: [
"in",
[
"Purchase Receipt",
"Purchase Invoice",
"Delivery Note",
"Sales Invoice",
"Stock Entry",
"Stock Reconciliation",
"Subcontracting Receipt",
],
],
},
};
});
@@ -25,97 +35,97 @@ frappe.ui.form.on('Repost Item Valuation', {
return {
filters: {
company: frm.doc.company,
docstatus: 1
}
docstatus: 1,
},
};
});
}
frm.trigger('setup_realtime_progress');
frm.trigger("setup_realtime_progress");
},
based_on: function(frm) {
based_on: function (frm) {
var fields_to_reset = [];
if (frm.doc.based_on == 'Transaction') {
fields_to_reset = ['item_code', 'warehouse'];
} else if (frm.doc.based_on == 'Item and Warehouse') {
fields_to_reset = ['voucher_type', 'voucher_no'];
if (frm.doc.based_on == "Transaction") {
fields_to_reset = ["item_code", "warehouse"];
} else if (frm.doc.based_on == "Item and Warehouse") {
fields_to_reset = ["voucher_type", "voucher_no"];
}
if (fields_to_reset) {
fields_to_reset.forEach(field => {
fields_to_reset.forEach((field) => {
frm.set_value(field, undefined);
});
}
},
setup_realtime_progress: function(frm) {
frappe.realtime.on('item_reposting_progress', data => {
setup_realtime_progress: function (frm) {
frappe.realtime.on("item_reposting_progress", (data) => {
if (frm.doc.name !== data.name) {
return;
}
if (frm.doc.status == 'In Progress') {
if (frm.doc.status == "In Progress") {
frm.doc.current_index = data.current_index;
frm.doc.items_to_be_repost = data.items_to_be_repost;
frm.dashboard.reset();
frm.trigger('show_reposting_progress');
frm.trigger("show_reposting_progress");
}
});
},
refresh: function(frm) {
if (frm.doc.status == "Failed" && frm.doc.docstatus==1) {
frm.add_custom_button(__('Restart'), function () {
refresh: function (frm) {
if (frm.doc.status == "Failed" && frm.doc.docstatus == 1) {
frm.add_custom_button(__("Restart"), function () {
frm.trigger("restart_reposting");
}).addClass("btn-primary");
}
frm.trigger('show_reposting_progress');
frm.trigger("show_reposting_progress");
if (frm.doc.status === 'Queued' && frm.doc.docstatus === 1) {
frm.trigger('execute_reposting');
if (frm.doc.status === "Queued" && frm.doc.docstatus === 1) {
frm.trigger("execute_reposting");
}
},
execute_reposting(frm) {
frm.add_custom_button(__("Start Reposting"), () => {
frappe.call({
method: 'erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation',
callback: function() {
frappe.msgprint(__('Reposting has been started in the background.'));
}
method: "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation",
callback: function () {
frappe.msgprint(__("Reposting has been started in the background."));
},
});
});
},
show_reposting_progress: function(frm) {
show_reposting_progress: function (frm) {
var bars = [];
let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0;
let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5;
var title = __('Reposting Completed {0}%', [progress]);
let progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5;
var title = __("Reposting Completed {0}%", [progress]);
bars.push({
'title': title,
'width': progress + '%',
'progress_class': 'progress-bar-success'
title: title,
width: progress + "%",
progress_class: "progress-bar-success",
});
frm.dashboard.add_progress(__('Reposting Progress'), bars);
frm.dashboard.add_progress(__("Reposting Progress"), bars);
},
restart_reposting: function(frm) {
restart_reposting: function (frm) {
frappe.call({
method: "restart_reposting",
doc: frm.doc,
callback: function(r) {
callback: function (r) {
if (!r.exc) {
frm.refresh();
}
}
},
});
}
},
});

View File

@@ -117,15 +117,12 @@ class RepostItemValuation(Document):
if not acc_settings.acc_frozen_upto:
return
if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto):
if (
if acc_settings.frozen_accounts_modifier and frappe.session.user in get_users_with_role(
acc_settings.frozen_accounts_modifier
and frappe.session.user in get_users_with_role(acc_settings.frozen_accounts_modifier)
):
frappe.msgprint(_("Caution: This might alter frozen accounts."))
return
frappe.throw(
_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto)
)
frappe.throw(_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto))
def reset_field_values(self):
if self.based_on == "Transaction":
@@ -173,14 +170,9 @@ class RepostItemValuation(Document):
if self.status not in ("Queued", "In Progress"):
return
if not (self.voucher_no and self.voucher_no):
return
transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus")
if transaction_status == 2:
msg = _("Cannot cancel as processing of cancelled documents is pending.")
msg += "<br>" + _("Please try again in an hour.")
frappe.throw(msg, title=_("Pending processing"))
msg = _("Cannot cancel as processing of cancelled documents is pending.")
msg += "<br>" + _("Please try again in an hour.")
frappe.throw(msg, title=_("Pending processing"))
@frappe.whitelist()
def restart_reposting(self):
@@ -226,6 +218,7 @@ def on_doctype_update():
def repost(doc):
try:
frappe.flags.through_repost_item_valuation = True
if not frappe.db.exists("Repost Item Valuation", doc.name):
return
@@ -248,7 +241,7 @@ def repost(doc):
raise
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
doc.log_error("Unable to repost item valuation")
message = frappe.message_log.pop() if frappe.message_log else ""

View File

@@ -179,7 +179,6 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
riv3.set_status("Skipped")
def test_stock_freeze_validation(self):
today = nowdate()
riv = frappe.get_doc(

View File

@@ -1,20 +1,20 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("customer", "customer_name", "customer_name")
cur_frm.add_fetch("supplier", "supplier_name", "supplier_name")
cur_frm.add_fetch("customer", "customer_name", "customer_name");
cur_frm.add_fetch("supplier", "supplier_name", "supplier_name");
cur_frm.add_fetch("item_code", "item_name", "item_name")
cur_frm.add_fetch("item_code", "description", "description")
cur_frm.add_fetch("item_code", "item_group", "item_group")
cur_frm.add_fetch("item_code", "brand", "brand")
cur_frm.add_fetch("item_code", "item_name", "item_name");
cur_frm.add_fetch("item_code", "description", "description");
cur_frm.add_fetch("item_code", "item_group", "item_group");
cur_frm.add_fetch("item_code", "brand", "brand");
cur_frm.cscript.onload = function() {
cur_frm.set_query("item_code", function() {
return erpnext.queries.item({"is_stock_item": 1, "has_serial_no": 1})
cur_frm.cscript.onload = function () {
cur_frm.set_query("item_code", function () {
return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 });
});
};
frappe.ui.form.on("Serial No", "refresh", function(frm) {
frappe.ui.form.on("Serial No", "refresh", function (frm) {
frm.toggle_enable("item_code", frm.doc.__islocal);
});

View File

@@ -3,7 +3,6 @@
import json
from typing import List, Optional, Union
import frappe
from frappe import ValidationError, _
@@ -66,7 +65,7 @@ class SerialNoDuplicateError(ValidationError):
class SerialNo(StockController):
def __init__(self, *args, **kwargs):
super(SerialNo, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.via_stock_ledger = False
def validate(self):
@@ -123,9 +122,7 @@ class SerialNo(StockController):
"""
item = frappe.get_cached_doc("Item", self.item_code)
if item.has_serial_no != 1:
frappe.throw(
_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)
)
frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code))
self.item_group = item.item_group
self.description = item.description
@@ -301,7 +298,8 @@ def validate_serial_no(sle, item_det):
if len(serial_nos) != len(set(serial_nos)):
frappe.throw(
_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError
_("Duplicate Serial No entered for Item {0}").format(sle.item_code),
SerialNoDuplicateError,
)
allow_existing_serial_no = cint(
@@ -337,7 +335,9 @@ def validate_serial_no(sle, item_det):
if sr.item_code != sle.item_code:
if not allow_serial_nos_with_different_item(serial_no, sle):
frappe.throw(
_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code),
_("Serial No {0} does not belong to Item {1}").format(
serial_no, sle.item_code
),
SerialNoItemError,
)
@@ -362,7 +362,9 @@ def validate_serial_no(sle, item_det):
frappe.throw(_(msg), SerialNoDuplicateError)
if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
doc_name = frappe.bold(
get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)
)
frappe.throw(
_("Serial No {0} has already been received in the {1} #{2}").format(
frappe.bold(serial_no), sr.purchase_document_type, doc_name
@@ -375,25 +377,32 @@ def validate_serial_no(sle, item_det):
and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"]
and sle.voucher_type == sr.delivery_document_type
):
return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against")
return_against = frappe.db.get_value(
sle.voucher_type, sle.voucher_no, "return_against"
)
if return_against and return_against != sr.delivery_document_no:
frappe.throw(_("Serial no {0} has been already returned").format(sr.name))
if cint(sle.actual_qty) < 0:
if sr.warehouse != sle.warehouse:
frappe.throw(
_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse),
_("Serial No {0} does not belong to Warehouse {1}").format(
serial_no, sle.warehouse
),
SerialNoWarehouseError,
)
if not sr.purchase_document_no:
frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
frappe.throw(
_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError
)
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
if sr.batch_no and sr.batch_no != sle.batch_no:
frappe.throw(
_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no),
_("Serial No {0} does not belong to Batch {1}").format(
serial_no, sle.batch_no
),
SerialNoBatchError,
)
@@ -408,7 +417,11 @@ def validate_serial_no(sle, item_det):
if sle.voucher_type == "Sales Invoice":
if not frappe.db.exists(
"Sales Invoice Item",
{"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order},
{
"parent": sle.voucher_no,
"item_code": sle.item_code,
"sales_order": sr.sales_order,
},
):
frappe.throw(
_(
@@ -431,7 +444,11 @@ def validate_serial_no(sle, item_det):
)
if not invoice or frappe.db.exists(
"Sales Invoice Item",
{"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order},
{
"parent": invoice,
"item_code": sle.item_code,
"sales_order": sr.sales_order,
},
):
frappe.throw(
_(
@@ -467,7 +484,9 @@ def validate_serial_no(sle, item_det):
{"parent": sales_invoice, "item_code": sle.item_code},
"sales_order",
)
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
if sales_order and get_reserved_qty_for_so(
sales_order, sle.item_code
):
validate_so_serial_no(sr, sales_order)
elif cint(sle.actual_qty) < 0:
# transfer out
@@ -483,9 +502,7 @@ def validate_serial_no(sle, item_det):
def check_serial_no_validity_on_cancel(serial_no, sle):
sr = frappe.db.get_value(
"Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1
)
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1)
sr_link = frappe.utils.get_link_to_form("Serial No", serial_no)
doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no)
actual_qty = cint(sle.actual_qty)
@@ -538,9 +555,7 @@ def validate_so_serial_no(sr, sales_order):
def has_serial_no_exists(sn, sle):
if (
sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation"
):
if sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation":
return True
if sn.company != sle.company:
@@ -584,7 +599,7 @@ def update_serial_nos(sle, item_det):
def get_auto_serial_nos(serial_no_series, qty):
serial_nos = []
for i in range(cint(qty)):
for _i in range(cint(qty)):
serial_nos.append(get_new_serial_number(serial_no_series))
return "\n".join(serial_nos)
@@ -634,13 +649,11 @@ def auto_make_serial_nos(args):
def get_items_html(serial_nos, item_code):
body = ", ".join(serial_nos)
return """<details><summary>
<b>{0}:</b> {1} Serial Numbers <span class="caret"></span>
return f"""<details><summary>
<b>{item_code}:</b> {len(serial_nos)} Serial Numbers <span class="caret"></span>
</summary>
<div class="small">{2}</div></details>
""".format(
item_code, len(serial_nos), body
)
<div class="small">{body}</div></details>
"""
def get_item_details(item_code):
@@ -657,9 +670,7 @@ def get_serial_nos(serial_no):
if isinstance(serial_no, list):
return serial_no
return [
s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
]
return [s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()]
def clean_serial_no_string(serial_no: str) -> str:
@@ -779,11 +790,9 @@ def update_maintenance_status():
def get_delivery_note_serial_no(item_code, qty, delivery_note):
serial_nos = ""
dn_serial_nos = frappe.db.sql_list(
""" select name from `tabSerial No`
f""" select name from `tabSerial No`
where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s
and sales_invoice is null limit {0}""".format(
cint(qty)
),
and sales_invoice is null limit {cint(qty)}""",
{"item_code": item_code, "delivery_note": delivery_note},
)
@@ -798,12 +807,11 @@ def auto_fetch_serial_number(
qty: int,
item_code: str,
warehouse: str,
posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None,
posting_date: str | None = None,
batch_nos: str | list[str] | None = None,
for_doctype: str | None = None,
exclude_sr_nos=None,
) -> List[str]:
) -> list[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
if exclude_sr_nos is None:

View File

@@ -1,14 +1,21 @@
frappe.listview_settings['Serial No'] = {
frappe.listview_settings["Serial No"] = {
add_fields: ["item_code", "warehouse", "warranty_expiry_date", "delivery_document_type"],
get_indicator: (doc) => {
if (doc.delivery_document_type) {
return [__("Delivered"), "green", "delivery_document_type,is,set"];
} else if (doc.warranty_expiry_date && frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0) {
return [__("Expired"), "red", "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set"];
} else if (
doc.warranty_expiry_date &&
frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0
) {
return [
__("Expired"),
"red",
"warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set",
];
} else if (!doc.warehouse) {
return [__("Inactive"), "grey", "warehouse,is,not set"];
} else {
return [__("Active"), "green", "delivery_document_type,is,not set"];
}
}
},
};

View File

@@ -248,16 +248,12 @@ class TestSerialNo(FrappeTestCase):
warehouse = "_Test Warehouse - _TC"
serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
in1 = make_stock_entry(
item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0]
)
in2 = make_stock_entry(
make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0])
make_stock_entry(
item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1]
)
out = create_delivery_note(
item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True
)
out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
# change serial no
out.items[0].serial_no = serial_nos[1]

View File

@@ -1,34 +1,44 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Shipment', {
address_query: function(frm, link_doctype, link_name, is_your_company_address) {
frappe.ui.form.on("Shipment", {
address_query: function (frm, link_doctype, link_name, is_your_company_address) {
return {
query: 'frappe.contacts.doctype.address.address.address_query',
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: link_doctype,
link_name: link_name,
is_your_company_address: is_your_company_address
}
is_your_company_address: is_your_company_address,
},
};
},
contact_query: function(frm, link_doctype, link_name) {
contact_query: function (frm, link_doctype, link_name) {
return {
query: 'frappe.contacts.doctype.contact.contact.contact_query',
query: "frappe.contacts.doctype.contact.contact.contact_query",
filters: {
link_doctype: link_doctype,
link_name: link_name
}
link_name: link_name,
},
};
},
onload: function(frm) {
onload: function (frm) {
frm.set_query("delivery_address_name", () => {
let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`;
return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0);
return frm.events.address_query(
frm,
frm.doc.delivery_to_type,
frm.doc[delivery_to],
frm.doc.delivery_to_type === "Company" ? 1 : 0
);
});
frm.set_query("pickup_address_name", () => {
let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`;
return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0);
return frm.events.address_query(
frm,
frm.doc.pickup_from_type,
frm.doc[pickup_from],
frm.doc.pickup_from_type === "Company" ? 1 : 0
);
});
frm.set_query("delivery_contact_name", () => {
let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`;
@@ -38,8 +48,8 @@ frappe.ui.form.on('Shipment', {
let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`;
return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]);
});
frm.set_query("delivery_note", "shipment_delivery_note", function() {
let customer = '';
frm.set_query("delivery_note", "shipment_delivery_note", function () {
let customer = "";
if (frm.doc.delivery_to_type == "Customer") {
customer = frm.doc.delivery_customer;
}
@@ -51,305 +61,329 @@ frappe.ui.form.on('Shipment', {
filters: {
customer: customer,
docstatus: 1,
status: ["not in", ["Cancelled"]]
}
status: ["not in", ["Cancelled"]],
},
};
}
});
},
refresh: function() {
$('div[data-fieldname=pickup_address] > div > .clearfix').hide();
$('div[data-fieldname=pickup_contact] > div > .clearfix').hide();
$('div[data-fieldname=delivery_address] > div > .clearfix').hide();
$('div[data-fieldname=delivery_contact] > div > .clearfix').hide();
refresh: function () {
$("div[data-fieldname=pickup_address] > div > .clearfix").hide();
$("div[data-fieldname=pickup_contact] > div > .clearfix").hide();
$("div[data-fieldname=delivery_address] > div > .clearfix").hide();
$("div[data-fieldname=delivery_contact] > div > .clearfix").hide();
},
before_save: function(frm) {
before_save: function (frm) {
let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`;
frm.set_value("delivery_to", frm.doc[delivery_to]);
let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`;
frm.set_value("pickup", frm.doc[pickup_from]);
},
set_pickup_company_address: function(frm) {
frappe.db.get_value('Address', {
address_title: frm.doc.pickup_company,
is_your_company_address: 1
}, 'name', (r) => {
frm.set_value("pickup_address_name", r.name);
});
set_pickup_company_address: function (frm) {
frappe.db.get_value(
"Address",
{
address_title: frm.doc.pickup_company,
is_your_company_address: 1,
},
"name",
(r) => {
frm.set_value("pickup_address_name", r.name);
}
);
},
set_delivery_company_address: function(frm) {
frappe.db.get_value('Address', {
address_title: frm.doc.delivery_company,
is_your_company_address: 1
}, 'name', (r) => {
frm.set_value("delivery_address_name", r.name);
});
set_delivery_company_address: function (frm) {
frappe.db.get_value(
"Address",
{
address_title: frm.doc.delivery_company,
is_your_company_address: 1,
},
"name",
(r) => {
frm.set_value("delivery_address_name", r.name);
}
);
},
pickup_from_type: function(frm) {
if (frm.doc.pickup_from_type == 'Company') {
frm.set_value("pickup_company", frappe.defaults.get_default('company'));
frm.set_value("pickup_customer", '');
frm.set_value("pickup_supplier", '');
pickup_from_type: function (frm) {
if (frm.doc.pickup_from_type == "Company") {
frm.set_value("pickup_company", frappe.defaults.get_default("company"));
frm.set_value("pickup_customer", "");
frm.set_value("pickup_supplier", "");
} else {
frm.trigger('clear_pickup_fields');
frm.trigger("clear_pickup_fields");
}
if (frm.doc.pickup_from_type == 'Customer') {
frm.set_value("pickup_company", '');
frm.set_value("pickup_supplier", '');
if (frm.doc.pickup_from_type == "Customer") {
frm.set_value("pickup_company", "");
frm.set_value("pickup_supplier", "");
}
if (frm.doc.pickup_from_type == 'Supplier') {
frm.set_value("pickup_customer", '');
frm.set_value("pickup_company", '');
if (frm.doc.pickup_from_type == "Supplier") {
frm.set_value("pickup_customer", "");
frm.set_value("pickup_company", "");
}
},
delivery_to_type: function(frm) {
if (frm.doc.delivery_to_type == 'Company') {
frm.set_value("delivery_company", frappe.defaults.get_default('company'));
frm.set_value("delivery_customer", '');
frm.set_value("delivery_supplier", '');
delivery_to_type: function (frm) {
if (frm.doc.delivery_to_type == "Company") {
frm.set_value("delivery_company", frappe.defaults.get_default("company"));
frm.set_value("delivery_customer", "");
frm.set_value("delivery_supplier", "");
} else {
frm.trigger('clear_delivery_fields');
frm.trigger("clear_delivery_fields");
}
if (frm.doc.delivery_to_type == 'Customer') {
frm.set_value("delivery_company", '');
frm.set_value("delivery_supplier", '');
if (frm.doc.delivery_to_type == "Customer") {
frm.set_value("delivery_company", "");
frm.set_value("delivery_supplier", "");
}
if (frm.doc.delivery_to_type == 'Supplier') {
frm.set_value("delivery_customer", '');
frm.set_value("delivery_company", '');
if (frm.doc.delivery_to_type == "Supplier") {
frm.set_value("delivery_customer", "");
frm.set_value("delivery_company", "");
frm.toggle_display("shipment_delivery_note", false);
} else {
frm.toggle_display("shipment_delivery_note", true);
}
},
delivery_address_name: function(frm) {
if (frm.doc.delivery_to_type == 'Company') {
erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true);
delivery_address_name: function (frm) {
if (frm.doc.delivery_to_type == "Company") {
erpnext.utils.get_address_display(frm, "delivery_address_name", "delivery_address", true);
} else {
erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false);
erpnext.utils.get_address_display(frm, "delivery_address_name", "delivery_address", false);
}
},
pickup_address_name: function(frm) {
if (frm.doc.pickup_from_type == 'Company') {
erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true);
pickup_address_name: function (frm) {
if (frm.doc.pickup_from_type == "Company") {
erpnext.utils.get_address_display(frm, "pickup_address_name", "pickup_address", true);
} else {
erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false);
erpnext.utils.get_address_display(frm, "pickup_address_name", "pickup_address", false);
}
},
get_contact_display: function(frm, contact_name, contact_type) {
get_contact_display: function (frm, contact_name, contact_type) {
frappe.call({
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
args: { contact: contact_name },
callback: function(r) {
callback: function (r) {
if (r.message) {
if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) {
if (contact_type == 'Delivery') {
frm.set_value('delivery_contact_name', '');
frm.set_value('delivery_contact', '');
if (contact_type == "Delivery") {
frm.set_value("delivery_contact_name", "");
frm.set_value("delivery_contact", "");
} else {
frm.set_value('pickup_contact_name', '');
frm.set_value('pickup_contact', '');
frm.set_value("pickup_contact_name", "");
frm.set_value("pickup_contact", "");
}
frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.")
+ "</br>" + __("Please set Email/Phone for the contact")
+ ` <a href='/app/contact/${contact_name}'>${contact_name}</a>`);
frappe.throw(
__("Email or Phone/Mobile of the Contact are mandatory to continue.") +
"</br>" +
__("Please set Email/Phone for the contact") +
` <a href='/app/contact/${contact_name}'>${contact_name}</a>`
);
}
let contact_display = r.message.contact_display;
if (r.message.contact_email) {
contact_display += '<br>' + r.message.contact_email;
contact_display += "<br>" + r.message.contact_email;
}
if (r.message.contact_phone) {
contact_display += '<br>' + r.message.contact_phone;
contact_display += "<br>" + r.message.contact_phone;
}
if (r.message.contact_mobile && !r.message.contact_phone) {
contact_display += '<br>' + r.message.contact_mobile;
contact_display += "<br>" + r.message.contact_mobile;
}
if (contact_type == 'Delivery') {
frm.set_value('delivery_contact', contact_display);
if (contact_type == "Delivery") {
frm.set_value("delivery_contact", contact_display);
if (r.message.contact_email) {
frm.set_value('delivery_contact_email', r.message.contact_email);
frm.set_value("delivery_contact_email", r.message.contact_email);
}
} else {
frm.set_value('pickup_contact', contact_display);
frm.set_value("pickup_contact", contact_display);
if (r.message.contact_email) {
frm.set_value('pickup_contact_email', r.message.contact_email);
frm.set_value("pickup_contact_email", r.message.contact_email);
}
}
}
}
},
});
},
delivery_contact_name: function(frm) {
delivery_contact_name: function (frm) {
if (frm.doc.delivery_contact_name) {
frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery');
frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, "Delivery");
}
},
pickup_contact_name: function(frm) {
pickup_contact_name: function (frm) {
if (frm.doc.pickup_contact_name) {
frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup');
frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, "Pickup");
}
},
pickup_contact_person: function(frm) {
pickup_contact_person: function (frm) {
if (frm.doc.pickup_contact_person) {
frappe.call({
method: "erpnext.stock.doctype.shipment.shipment.get_company_contact",
args: { user: frm.doc.pickup_contact_person },
callback: function({ message }) {
callback: function ({ message }) {
const r = message;
let contact_display = `${r.first_name} ${r.last_name}`;
if (r.email) {
contact_display += `<br>${ r.email }`;
frm.set_value('pickup_contact_email', r.email);
contact_display += `<br>${r.email}`;
frm.set_value("pickup_contact_email", r.email);
}
if (r.phone) {
contact_display += `<br>${ r.phone }`;
contact_display += `<br>${r.phone}`;
}
if (r.mobile_no && !r.phone) {
contact_display += `<br>${ r.mobile_no }`;
contact_display += `<br>${r.mobile_no}`;
}
frm.set_value('pickup_contact', contact_display);
}
frm.set_value("pickup_contact", contact_display);
},
});
} else {
if (frm.doc.pickup_from_type === 'Company') {
if (frm.doc.pickup_from_type === "Company") {
frappe.call({
method: "erpnext.stock.doctype.shipment.shipment.get_company_contact",
args: { user: frappe.session.user },
callback: function({ message }) {
callback: function ({ message }) {
const r = message;
let contact_display = `${r.first_name} ${r.last_name}`;
if (r.email) {
contact_display += `<br>${ r.email }`;
frm.set_value('pickup_contact_email', r.email);
contact_display += `<br>${r.email}`;
frm.set_value("pickup_contact_email", r.email);
}
if (r.phone) {
contact_display += `<br>${ r.phone }`;
contact_display += `<br>${r.phone}`;
}
if (r.mobile_no && !r.phone) {
contact_display += `<br>${ r.mobile_no }`;
contact_display += `<br>${r.mobile_no}`;
}
frm.set_value('pickup_contact', contact_display);
}
frm.set_value("pickup_contact", contact_display);
},
});
}
}
},
set_company_contact: function(frm, delivery_type) {
frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => {
if (!(r.last_name && r.email && (r.phone || r.mobile_no))) {
if (delivery_type == 'Delivery') {
frm.set_value('delivery_company', '');
frm.set_value('delivery_contact', '');
set_company_contact: function (frm, delivery_type) {
frappe.db.get_value(
"User",
{ name: frappe.session.user },
["full_name", "last_name", "email", "phone", "mobile_no"],
(r) => {
if (!(r.last_name && r.email && (r.phone || r.mobile_no))) {
if (delivery_type == "Delivery") {
frm.set_value("delivery_company", "");
frm.set_value("delivery_contact", "");
} else {
frm.set_value("pickup_company", "");
frm.set_value("pickup_contact", "");
}
frappe.throw(
__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") +
"</br>" +
__("Please first set Last Name, Email and Phone for the user") +
` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>`
);
}
let contact_display = r.full_name;
if (r.email) {
contact_display += "<br>" + r.email;
}
if (r.phone) {
contact_display += "<br>" + r.phone;
}
if (r.mobile_no && !r.phone) {
contact_display += "<br>" + r.mobile_no;
}
if (delivery_type == "Delivery") {
frm.set_value("delivery_contact", contact_display);
if (r.email) {
frm.set_value("delivery_contact_email", r.email);
}
} else {
frm.set_value('pickup_company', '');
frm.set_value('pickup_contact', '');
}
frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "</br>"
+ __("Please first set Last Name, Email and Phone for the user")
+ ` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>`);
}
let contact_display = r.full_name;
if (r.email) {
contact_display += '<br>' + r.email;
}
if (r.phone) {
contact_display += '<br>' + r.phone;
}
if (r.mobile_no && !r.phone) {
contact_display += '<br>' + r.mobile_no;
}
if (delivery_type == 'Delivery') {
frm.set_value('delivery_contact', contact_display);
if (r.email) {
frm.set_value('delivery_contact_email', r.email);
}
} else {
frm.set_value('pickup_contact', contact_display);
if (r.email) {
frm.set_value('pickup_contact_email', r.email);
frm.set_value("pickup_contact", contact_display);
if (r.email) {
frm.set_value("pickup_contact_email", r.email);
}
}
}
});
frm.set_value('pickup_contact_person', frappe.session.user);
);
frm.set_value("pickup_contact_person", frappe.session.user);
},
pickup_company: function(frm) {
if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) {
frm.trigger('set_pickup_company_address');
frm.events.set_company_contact(frm, 'Pickup');
pickup_company: function (frm) {
if (frm.doc.pickup_from_type == "Company" && frm.doc.pickup_company) {
frm.trigger("set_pickup_company_address");
frm.events.set_company_contact(frm, "Pickup");
}
},
delivery_company: function(frm) {
if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) {
frm.trigger('set_delivery_company_address');
frm.events.set_company_contact(frm, 'Delivery');
delivery_company: function (frm) {
if (frm.doc.delivery_to_type == "Company" && frm.doc.delivery_company) {
frm.trigger("set_delivery_company_address");
frm.events.set_company_contact(frm, "Delivery");
}
},
delivery_customer: function(frm) {
frm.trigger('clear_delivery_fields');
delivery_customer: function (frm) {
frm.trigger("clear_delivery_fields");
if (frm.doc.delivery_customer) {
frm.events.set_address_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery');
frm.events.set_contact_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery');
frm.events.set_address_name(frm, "Customer", frm.doc.delivery_customer, "Delivery");
frm.events.set_contact_name(frm, "Customer", frm.doc.delivery_customer, "Delivery");
}
},
delivery_supplier: function(frm) {
frm.trigger('clear_delivery_fields');
delivery_supplier: function (frm) {
frm.trigger("clear_delivery_fields");
if (frm.doc.delivery_supplier) {
frm.events.set_address_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery');
frm.events.set_contact_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery');
frm.events.set_address_name(frm, "Supplier", frm.doc.delivery_supplier, "Delivery");
frm.events.set_contact_name(frm, "Supplier", frm.doc.delivery_supplier, "Delivery");
}
},
pickup_customer: function(frm) {
pickup_customer: function (frm) {
if (frm.doc.pickup_customer) {
frm.events.set_address_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup');
frm.events.set_contact_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup');
frm.events.set_address_name(frm, "Customer", frm.doc.pickup_customer, "Pickup");
frm.events.set_contact_name(frm, "Customer", frm.doc.pickup_customer, "Pickup");
}
},
pickup_supplier: function(frm) {
pickup_supplier: function (frm) {
if (frm.doc.pickup_supplier) {
frm.events.set_address_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup');
frm.events.set_contact_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup');
frm.events.set_address_name(frm, "Supplier", frm.doc.pickup_supplier, "Pickup");
frm.events.set_contact_name(frm, "Supplier", frm.doc.pickup_supplier, "Pickup");
}
},
set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) {
set_address_name: function (frm, ref_doctype, ref_docname, delivery_type) {
frappe.call({
method: "erpnext.stock.doctype.shipment.shipment.get_address_name",
args: {
ref_doctype: ref_doctype,
docname: ref_docname
docname: ref_docname,
},
callback: function(r) {
callback: function (r) {
if (r.message) {
if (delivery_type == 'Delivery') {
frm.set_value('delivery_address_name', r.message);
if (delivery_type == "Delivery") {
frm.set_value("delivery_address_name", r.message);
} else {
frm.set_value('pickup_address_name', r.message);
frm.set_value("pickup_address_name", r.message);
}
}
}
},
});
},
set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) {
set_contact_name: function (frm, ref_doctype, ref_docname, delivery_type) {
frappe.call({
method: "erpnext.stock.doctype.shipment.shipment.get_contact_name",
args: {
ref_doctype: ref_doctype,
docname: ref_docname
docname: ref_docname,
},
callback: function(r) {
callback: function (r) {
if (r.message) {
if (delivery_type == 'Delivery') {
frm.set_value('delivery_contact_name', r.message);
if (delivery_type == "Delivery") {
frm.set_value("delivery_contact_name", r.message);
} else {
frm.set_value('pickup_contact_name', r.message);
frm.set_value("pickup_contact_name", r.message);
}
}
}
},
});
},
add_template: function(frm) {
add_template: function (frm) {
if (frm.doc.parcel_template) {
frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => {
let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template);
let parcel_template = frappe.model.get_doc(
"Shipment Parcel Template",
frm.doc.parcel_template
);
let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel");
row.length = parcel_template.length;
row.width = parcel_template.width;
@@ -359,56 +393,71 @@ frappe.ui.form.on('Shipment', {
});
}
},
pickup_date: function(frm) {
pickup_date: function (frm) {
if (frm.doc.pickup_date < frappe.datetime.get_today()) {
frappe.throw(__("Pickup Date cannot be before this day"));
}
},
clear_pickup_fields: function(frm) {
let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"];
clear_pickup_fields: function (frm) {
let fields = [
"pickup_address_name",
"pickup_contact_name",
"pickup_address",
"pickup_contact",
"pickup_contact_email",
"pickup_contact_person",
];
for (let field of fields) {
frm.set_value(field, '');
frm.set_value(field, "");
}
},
clear_delivery_fields: function(frm) {
let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"];
clear_delivery_fields: function (frm) {
let fields = [
"delivery_address_name",
"delivery_contact_name",
"delivery_address",
"delivery_contact",
"delivery_contact_email",
];
for (let field of fields) {
frm.set_value(field, '');
frm.set_value(field, "");
}
},
remove_email_row: function(frm, table, fieldname) {
$.each(frm.doc[table] || [], function(i, detail) {
remove_email_row: function (frm, table, fieldname) {
$.each(frm.doc[table] || [], function (i, detail) {
if (detail.email === fieldname) {
cur_frm.get_field(table).grid.grid_rows[i].remove();
}
});
}
},
});
frappe.ui.form.on('Shipment Delivery Note', {
delivery_note: function(frm, cdt, cdn) {
frappe.ui.form.on("Shipment Delivery Note", {
delivery_note: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.delivery_note) {
let row_index = row.idx - 1;
if (validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) {
frappe.throw(__("You have entered a duplicate Delivery Note on Row") + ` ${row.idx}. ` + __("Please rectify and try again."));
if (validate_duplicate(frm, "shipment_delivery_note", row.delivery_note, row_index)) {
frappe.throw(
__("You have entered a duplicate Delivery Note on Row") +
` ${row.idx}. ` +
__("Please rectify and try again.")
);
}
}
},
grand_total: function(frm, cdt, cdn) {
grand_total: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.grand_total) {
var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total);
var value_of_goods = parseFloat(frm.doc.value_of_goods) + parseFloat(row.grand_total);
frm.set_value("value_of_goods", Math.round(value_of_goods));
frm.refresh_fields("value_of_goods");
}
},
});
var validate_duplicate = function(frm, table, fieldname, index) {
return (
table === 'shipment_delivery_note'
? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i))
: frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i))
);
var validate_duplicate = function (frm, table, fieldname, index) {
return table === "shipment_delivery_note"
? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i))
: frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i));
};

View File

@@ -1,8 +1,8 @@
frappe.listview_settings['Shipment'] = {
frappe.listview_settings["Shipment"] = {
add_fields: ["status"],
get_indicator: function(doc) {
if (doc.status=='Booked') {
get_indicator: function (doc) {
if (doc.status == "Booked") {
return [__("Booked"), "green"];
}
}
},
};

View File

@@ -76,9 +76,7 @@ def create_test_shipment(delivery_notes=None):
shipment.description_of_content = "unit test entry"
for delivery_note in delivery_notes:
shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name})
shipment.append(
"shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5}
)
shipment.append("shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5})
shipment.insert()
return shipment
@@ -96,9 +94,7 @@ def get_shipment_customer_contact(customer_name):
def get_shipment_customer_address(customer_name):
address_title = customer_name + " address 123"
customer_address = frappe.get_all(
"Address", fields=["name"], filters={"address_title": address_title}
)
customer_address = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title})
if len(customer_address):
return customer_address[0]
else:
@@ -160,9 +156,7 @@ def create_customer_contact(fname, lname):
customer.is_primary_contact = 1
customer.is_billing_contact = 1
customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1})
customer.append(
"phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1}
)
customer.append("phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1})
customer.status = "Passive"
customer.insert()
return customer

View File

@@ -16,22 +16,19 @@
"fieldname": "length",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Length (cm)",
"reqd": 1
"label": "Length (cm)"
},
{
"fieldname": "width",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Width (cm)",
"reqd": 1
"label": "Width (cm)"
},
{
"fieldname": "height",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Height (cm)",
"reqd": 1
"label": "Height (cm)"
},
{
"fieldname": "weight",
@@ -52,7 +49,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-07-09 12:54:14.847170",
"modified": "2024-03-06 16:48:57.355757",
"modified_by": "Administrator",
"module": "Stock",
"name": "Shipment Parcel",
@@ -61,5 +58,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Shipment Parcel Template', {
frappe.ui.form.on("Shipment Parcel Template", {
// refresh: function(frm) {
// }
});

View File

@@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', {
},
callback: function(r) {
if (!r.exc) {
["actual_qty", "basic_rate"].forEach((field) => {
let fields = ["actual_qty", "basic_rate"];
if (frm.doc.purpose == "Material Receipt") {
fields = ["actual_qty"];
}
fields.forEach((field) => {
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
});
frm.events.calculate_basic_amount(frm, child);
@@ -543,7 +548,9 @@ frappe.ui.form.on('Stock Entry', {
let fields = [
{"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"),
options:"BOM", reqd: 1, get_query: filters()},
options:"BOM", reqd: 1, get_query: () => {
return {filters: { docstatus:1, "is_active": 1 }};
}},
{"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"),
options:"Warehouse"},
{"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"),

View File

@@ -104,7 +104,8 @@
"in_standard_filter": 1,
"label": "Stock Entry Type",
"options": "Stock Entry Type",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"depends_on": "eval:doc.purpose == 'Material Transfer'",
@@ -546,7 +547,8 @@
"label": "Job Card",
"options": "Job Card",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "amended_from",
@@ -679,7 +681,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-19 18:23:40.748114",
"modified": "2024-01-12 11:56:58.644882",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@@ -9,22 +9,17 @@ import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
getdate,
month_diff,
nowdate,
)
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
get_op_cost_from_sub_assemblies,
get_scrap_items_from_sub_assemblies,
validate_bom_no,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos
@@ -37,6 +32,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
OpeningEntryAccountError,
)
from erpnext.stock.get_item_details import (
get_barcode_data,
get_bin_details,
get_conversion_factor,
get_default_cost_center,
@@ -73,7 +69,7 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
class StockEntry(StockController):
def __init__(self, *args, **kwargs):
super(StockEntry, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
if self.purchase_order:
self.subcontract_data = frappe._dict(
{
@@ -103,9 +99,7 @@ class StockEntry(StockController):
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"]
)
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)
@@ -154,7 +148,6 @@ class StockEntry(StockController):
set_batch_nos(self, "s_warehouse")
self.validate_serialized_batch()
self.set_actual_qty()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
@@ -164,41 +157,6 @@ class StockEntry(StockController):
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def submit(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
)
)
self.queue_action("submit", timeout=2000)
else:
self._submit()
def cancel(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
)
)
self.queue_action("cancel", timeout=2000)
else:
self._cancel()
def is_enqueue_action(self, force=False) -> bool:
if force:
return True
if frappe.flags.in_test:
return False
# If line items are more than 100 or record is older than 6 months
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
return True
return False
def on_submit(self):
self.update_stock_ledger()
@@ -309,7 +267,11 @@ class StockEntry(StockController):
if self.purpose == "Send to Warehouse":
for d in frappe.get_all(
"Stock Entry",
filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"},
filters={
"docstatus": 0,
"outgoing_stock_entry": self.name,
"purpose": "Receive at Warehouse",
},
):
frappe.delete_doc("Stock Entry", d.name)
@@ -393,7 +355,14 @@ class StockEntry(StockController):
for field in reset_fields:
item.set(field, item_details.get(field))
update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
update_fields = (
"uom",
"description",
"expense_account",
"cost_center",
"conversion_factor",
"barcode",
)
for field in update_fields:
if not item.get(field):
@@ -463,7 +432,7 @@ class StockEntry(StockController):
return
precision = frappe.get_precision("Stock Entry Detail", "qty")
fg_item = list(fg_qty.keys())[0]
fg_item = next(iter(fg_qty.keys()))
fg_item_qty = flt(fg_qty[fg_item], precision)
fg_completed_qty = flt(self.fg_completed_qty, precision)
@@ -646,14 +615,13 @@ class StockEntry(StockController):
production_item, qty = frappe.db.get_value(
"Work Order", self.work_order, ["production_item", "qty"]
)
args = other_ste + [production_item]
args = [*other_ste, production_item]
fg_qty_already_entered = frappe.db.sql(
"""select sum(transfer_qty)
from `tabStock Entry Detail`
where parent in (%s)
and item_code = %s
and ifnull(s_warehouse,'')='' """
% (", ".join(["%s" * len(other_ste)]), "%s"),
where parent in ({})
and item_code = {}
and ifnull(s_warehouse,'')='' """.format(", ".join(["%s" * len(other_ste)]), "%s"),
args,
)[0][0]
if fg_qty_already_entered and fg_qty_already_entered >= qty:
@@ -731,9 +699,7 @@ class StockEntry(StockController):
Set rate for outgoing, scrapped and finished items
"""
# Set rate for outgoing items
outgoing_items_cost = self.set_rate_for_outgoing_items(
reset_outgoing_rate, raise_error_if_no_rate
)
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
items = []
@@ -897,8 +863,6 @@ class StockEntry(StockController):
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
def validate_duplicate_serial_no(self):
warehouse_wise_serial_nos = {}
# In case of repack the source and target serial nos could be same
for warehouse in ["s_warehouse", "t_warehouse"]:
serial_nos = []
@@ -936,13 +900,17 @@ class StockEntry(StockController):
item_code = se_item.original_item or se_item.item_code
precision = cint(frappe.db.get_default("float_precision")) or 3
required_qty = sum(
[flt(d.required_qty) for d in subcontract_order.supplied_items if d.rm_item_code == item_code]
[
flt(d.required_qty)
for d in subcontract_order.supplied_items
if d.rm_item_code == item_code
]
)
total_allowed = required_qty + (required_qty * (qty_allowance / 100))
if not required_qty:
bom_no = frappe.db.get_value(
frappe.db.get_value(
f"{self.subcontract_data.order_doctype} Item",
{
"parent": self.get(self.subcontract_data.order_field),
@@ -988,7 +956,10 @@ class StockEntry(StockController):
& (se.docstatus == 1)
& (se_detail.item_code == se_item.item_code)
& (
((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail))
(
(se.purchase_order == self.purchase_order)
& (se_detail.po_detail == se_item.po_detail)
)
if self.subcontract_data.order_doctype == "Purchase Order"
else (
(se.subcontracting_order == self.subcontracting_order)
@@ -1041,7 +1012,9 @@ class StockEntry(StockController):
else:
if not se_item.allow_alternative_item:
frappe.throw(
_("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
_(
"Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}"
).format(
se_item.idx,
se_item.item_code,
self.subcontract_data.order_doctype,
@@ -1228,12 +1201,18 @@ class StockEntry(StockController):
for d in self.get("items"):
if cstr(d.s_warehouse):
sle = self.get_sl_entries(
d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0}
d,
{
"warehouse": cstr(d.s_warehouse),
"actual_qty": -flt(d.transfer_qty),
"incoming_rate": 0,
},
)
if cstr(d.t_warehouse):
sle.dependant_sle_voucher_detail_no = d.name
elif finished_item_row and (
finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse
finished_item_row.item_code != d.item_code
or finished_item_row.t_warehouse != d.s_warehouse
):
sle.dependant_sle_voucher_detail_no = finished_item_row.name
@@ -1256,7 +1235,7 @@ class StockEntry(StockController):
sl_entries.append(sle)
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
gl_entries = super().get_gl_entries(warehouse_account)
if self.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
@@ -1289,9 +1268,9 @@ class StockEntry(StockController):
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
)
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account][
"base_amount"
] += flt(t.base_amount * multiply_based_on) / divide_based_on
if item_account_wise_additional_cost:
for d in self.get("items"):
@@ -1323,7 +1302,9 @@ class StockEntry(StockController):
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": -1
* amount["base_amount"], # put it as negative credit instead of debit purposefully
* amount[
"base_amount"
], # put it as negative credit instead of debit purposefully
},
item=d,
)
@@ -1421,11 +1402,7 @@ class StockEntry(StockController):
ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty")))
if self.purpose == "Material Issue":
ret["expense_account"] = (
item.get("expense_account")
or item_group_defaults.get("expense_account")
or frappe.get_cached_value("Company", self.company, "default_expense_account")
)
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
for company_field, field in {
"stock_adjustment_account": "expense_account",
@@ -1456,13 +1433,20 @@ class StockEntry(StockController):
):
subcontract_items = frappe.get_all(
self.subcontract_data.order_supplied_items_field,
{"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")},
{
"parent": self.get(self.subcontract_data.order_field),
"rm_item_code": args.get("item_code"),
},
"main_item_code",
)
if subcontract_items and len(subcontract_items) == 1:
ret["subcontracted_item"] = subcontract_items[0].main_item_code
barcode_data = get_barcode_data(item_code=item.name)
if barcode_data and len(barcode_data.get(item.name)) == 1:
ret["barcode"] = barcode_data.get(item.name)[0]
return ret
@frappe.whitelist()
@@ -1506,7 +1490,6 @@ class StockEntry(StockController):
)
if self.bom_no:
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
@@ -1520,7 +1503,6 @@ class StockEntry(StockController):
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
]:
if self.work_order and self.purpose == "Material Transfer for Manufacture":
item_dict = self.get_pending_raw_materials(backflush_based_on)
if self.to_warehouse and self.pro_doc:
@@ -1530,7 +1512,10 @@ class StockEntry(StockController):
elif (
self.work_order
and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
and (
self.purpose == "Manufacture"
or self.purpose == "Material Consumption for Manufacture"
)
and not self.pro_doc.skip_transfer
and self.flags.backflush_based_on == "Material Transferred for Manufacture"
):
@@ -1538,7 +1523,10 @@ class StockEntry(StockController):
elif (
self.work_order
and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
and (
self.purpose == "Manufacture"
or self.purpose == "Material Consumption for Manufacture"
)
and self.flags.backflush_based_on == "BOM"
and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1
):
@@ -1551,7 +1539,10 @@ class StockEntry(StockController):
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
# Get Subcontract Order Supplied Items Details
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
if (
self.get(self.subcontract_data.order_field)
and self.purpose == "Send to Subcontractor"
):
# Get Subcontract Order Supplied Items Details
parent = frappe.qb.DocType(self.subcontract_data.order_doctype)
child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field)
@@ -1570,9 +1561,14 @@ class StockEntry(StockController):
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
item["from_warehouse"] = self.pro_doc.wip_warehouse
# Get Reserve Warehouse from Subcontract Order
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
if (
self.get(self.subcontract_data.order_field)
and self.purpose == "Send to Subcontractor"
):
item["from_warehouse"] = item_wh.get(item.item_code)
item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
item["to_warehouse"] = (
self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
)
self.add_to_stock_entry_detail(item_dict)
@@ -1767,11 +1763,20 @@ class StockEntry(StockController):
def get_bom_scrap_material(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
or {}
)
if (
frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies")
and self.work_order
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
):
item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty)
else:
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(
self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
)
or {}
)
for item in item_dict.values():
item.from_warehouse = ""
@@ -1915,7 +1920,7 @@ class StockEntry(StockController):
as_dict=1,
)
for key, row in available_materials.items():
for _key, row in available_materials.items():
remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty)
if remaining_qty_to_produce <= 0 and not self.is_return:
continue
@@ -2059,9 +2064,7 @@ class StockEntry(StockController):
continue
transfer_pending = flt(d.required_qty) > flt(d.transferred_qty)
can_transfer = transfer_pending or (
backflush_based_on == "Material Transferred for Manufacture"
)
can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture")
if not can_transfer:
continue
@@ -2076,7 +2079,9 @@ class StockEntry(StockController):
)
item_row["job_card_item"] = job_card_item or None
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
if d.source_warehouse and not frappe.db.get_value(
"Warehouse", d.source_warehouse, "is_group"
):
item_row["from_warehouse"] = d.source_warehouse
item_row["to_warehouse"] = wip_warehouse
@@ -2131,9 +2136,9 @@ class StockEntry(StockController):
if item_row.get(field):
se_child.set(field, item_row.get(field))
if se_child.s_warehouse == None:
if se_child.s_warehouse is None:
se_child.s_warehouse = self.from_warehouse
if se_child.t_warehouse == None:
if se_child.t_warehouse is None:
se_child.t_warehouse = self.to_warehouse
# in stock uom
@@ -2189,15 +2194,20 @@ class StockEntry(StockController):
expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date")
if expiry_date:
if getdate(self.posting_date) > getdate(expiry_date):
frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
frappe.throw(
_("Batch {0} of Item {1} has expired.").format(
item.batch_no, item.item_code
)
)
else:
frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
frappe.throw(
_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)
)
def update_subcontract_order_supplied_items(self):
if self.get(self.subcontract_data.order_field) and (
self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
):
# Get Subcontract Order Supplied Items Details
order_supplied_items = frappe.db.get_all(
self.subcontract_data.order_supplied_items_field,
@@ -2298,8 +2308,8 @@ class StockEntry(StockController):
cond = ""
for data, transferred_qty in stock_entries.items():
cond += """ WHEN (parent = %s and name = %s) THEN %s
""" % (
cond += """ WHEN (parent = {} and name = {}) THEN {}
""".format(
frappe.db.escape(data[0]),
frappe.db.escape(data[1]),
transferred_qty,
@@ -2355,7 +2365,9 @@ class StockEntry(StockController):
material_request = item.material_request or None
if self.purpose == "Material Transfer" and material_request not in material_requests:
if self.outgoing_stock_entry and parent_se:
material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request")
material_request = frappe.get_value(
"Stock Entry Detail", item.ste_detail, "material_request"
)
if material_request and material_request not in material_requests:
material_requests.append(material_request)
@@ -2527,6 +2539,15 @@ def get_work_order_details(work_order, company):
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:
if (
bom_no
and frappe.db.get_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
)
and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom")
):
return get_op_cost_from_sub_assemblies(bom_no)
if not bom_no:
bom_no = work_order.bom_no
@@ -2551,9 +2572,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
)
)
):
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(
work_order.produced_qty
)
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
return operating_cost_per_unit
@@ -2566,24 +2585,20 @@ def get_used_alternative_items(
if subcontract_order:
cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'"
elif work_order:
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
work_order
)
cond = f"and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{work_order}'"
if not cond:
return {}
used_alternative_items = {}
data = frappe.db.sql(
""" select sted.original_item, sted.uom, sted.conversion_factor,
f""" select sted.original_item, sted.uom, sted.conversion_factor,
sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description
from
`tabStock Entry` ste, `tabStock Entry Detail` sted
where
sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code
{0} """.format(
cond
),
{cond} """,
as_dict=1,
)
@@ -2621,9 +2636,7 @@ def get_uom_details(item_code, uom, qty):
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
if not conversion_factor:
frappe.msgprint(
_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
)
frappe.msgprint(_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code))
ret = {"uom": ""}
else:
ret = {
@@ -2733,9 +2746,7 @@ def get_supplied_items(
else:
supplied_item.supplied_qty += row.transfer_qty
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(
supplied_item.returned_qty
)
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
return supplied_item_details
@@ -2825,7 +2836,11 @@ def get_stock_entry_data(work_order):
& (stock_entry_detail.s_warehouse.isnotnull())
& (
stock_entry.purpose.isin(
["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"]
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
)

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