Merge pull request #32375 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2022-09-28 15:17:26 +05:30
committed by GitHub
36 changed files with 441 additions and 258 deletions

View File

@@ -39,7 +39,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
container: [1, 2, 3, 4] container: [1, 2, 3]
name: Python Unit Tests name: Python Unit Tests

View File

@@ -14,6 +14,7 @@ from erpnext.accounts.utils import (
QueryPaymentLedger, QueryPaymentLedger,
get_outstanding_invoices, get_outstanding_invoices,
reconcile_against_document, reconcile_against_document,
update_reference_in_payment_entry,
) )
from erpnext.controllers.accounts_controller import get_advance_payment_entries from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -212,6 +213,23 @@ class PaymentReconciliation(Document):
inv.currency = entry.get("currency") inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount")) inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, allocated_entry):
if allocated_entry.get("reference_type") != "Payment Entry":
return
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
row = self.get_payment_details(allocated_entry, dr_or_cr)
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
update_reference_in_payment_entry(row, doc, do_not_save=True)
return doc.difference_amount
@frappe.whitelist() @frappe.whitelist()
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()
@@ -227,12 +245,16 @@ class PaymentReconciliation(Document):
res = self.get_allocated_entry(pay, inv, pay["amount"]) res = self.get_allocated_entry(pay, inv, pay["amount"])
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0 pay["amount"] = 0
res.difference_amount = self.get_difference_amount(res)
if pay.get("amount") == 0: if pay.get("amount") == 0:
entries.append(res) entries.append(res)
break break
elif inv.get("outstanding_amount") == 0: elif inv.get("outstanding_amount") == 0:
entries.append(res) entries.append(res)
continue continue
else: else:
break break

View File

@@ -1553,7 +1553,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-03-22 13:00:24.166684", "modified": "2022-09-27 13:00:24.166684",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
frappe.bold(d.warehouse), frappe.bold(d.warehouse),
frappe.bold(d.qty), frappe.bold(d.qty),
) )
if flt(available_stock) <= 0: if is_stock_item and flt(available_stock) <= 0:
frappe.throw( frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format( _("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse d.idx, item_code, warehouse
), ),
title=_("Item Unavailable"), title=_("Item Unavailable"),
) )
elif flt(available_stock) < flt(d.qty): elif is_stock_item and flt(available_stock) < flt(d.qty):
frappe.throw( frappe.throw(
_( _(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -632,11 +632,12 @@ def get_stock_availability(item_code, warehouse):
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item return bin_qty - pos_sales_qty, is_stock_item
else: else:
is_stock_item = False is_stock_item = True
if frappe.db.exists("Product Bundle", item_code): if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item return get_bundle_availability(item_code, warehouse), is_stock_item
else: else:
# Is a service item is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item return 0, is_stock_item
@@ -650,7 +651,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
available_qty = item_bin_qty - item_pos_reserved_qty available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles: if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
bundle_bin_qty = max_available_bundles bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
@@ -740,3 +743,7 @@ def add_return_modes(doc, pos_profile):
]: ]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0]) append_payment(payment_mode[0])
def on_doctype_update():
frappe.db.add_index("POS Invoice", ["return_against"])

View File

@@ -25,7 +25,7 @@
</div> </div>
<br> <br>
<table class="table table-bordered"> <table class="table table-bordered" style="font-size: 10px">
<thead> <thead>
<tr> <tr>
<th style="width: 12%">{{ _("Date") }}</th> <th style="width: 12%">{{ _("Date") }}</th>

View File

@@ -1543,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.save() pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000) self.assertEqual(pi.items[0].conversion_factor, 1000)
def test_batch_expiry_for_purchase_invoice(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
item = self.make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pi = make_purchase_invoice(
qty=1,
item_code=item.name,
update_stock=True,
)
pi.load_from_db()
batch_no = pi.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save().submit()
self.assertTrue(return_pi.docstatus == 1)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(

View File

@@ -1081,7 +1081,7 @@ class SalesInvoice(SellingController):
if self.is_return: if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain( fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset, item.base_net_amount, item.finance_book asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
) )
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
@@ -1091,7 +1091,7 @@ class SalesInvoice(SellingController):
else: else:
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.base_net_amount, item.finance_book asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
) )
asset.db_set("disposal_date", self.posting_date) asset.db_set("disposal_date", self.posting_date)

View File

@@ -237,9 +237,9 @@ def get_conditions(filters):
or filters.get("party") or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"] or filters.get("group_by") in ["Group by Account", "Group by Party"]
): ):
conditions.append("posting_date >=%(from_date)s") conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") conditions.append("(posting_date <=%(to_date)s)")
if filters.get("project"): if filters.get("project"):
conditions.append("project in %(project)s") conditions.append("project in %(project)s")

View File

@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
for d in data: for d in data:
for period in period_list: for period in period_list:
key = period if consolidated else period.key key = period if consolidated else period.key
d[key] = totals[d["account"]]
d["total"] = totals[d["account"]] d["total"] = totals[d["account"]]
return data return data

View File

@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
query_filters = { query_filters = {
"company": filters.company, "company": filters.company,
"from_date": filters.from_date, "from_date": filters.from_date,
"to_date": filters.to_date,
"report_type": report_type, "report_type": report_type,
"year_start_date": filters.year_start_date, "year_start_date": filters.year_start_date,
"project": filters.project, "project": filters.project,
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
where where
company=%(company)s company=%(company)s
{additional_conditions} {additional_conditions}
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
and account in (select name from `tabAccount` where report_type=%(report_type)s) and account in (select name from `tabAccount` where report_type=%(report_type)s)
and is_cancelled = 0 and is_cancelled = 0
group by account""".format( group by account""".format(

View File

@@ -104,12 +104,17 @@ def get_opening_balances(filters):
where company=%(company)s where company=%(company)s
and is_cancelled=0 and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
{account_filter} {account_filter}
group by party""".format( group by party""".format(
account_filter=account_filter account_filter=account_filter
), ),
{"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type}, {
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"party_type": filters.party_type,
},
as_dict=True, as_dict=True,
) )

View File

@@ -230,7 +230,7 @@ frappe.ui.form.on('Asset', {
datasets: [{ datasets: [{
color: 'green', color: 'green',
values: asset_values, values: asset_values,
formatted: asset_values.map(d => d.toFixed(2)) formatted: asset_values.map(d => d?.toFixed(2))
}] }]
}, },
type: 'line' type: 'line'

View File

@@ -235,7 +235,9 @@ def restore_asset(asset_name):
asset.set_status() asset.set_status()
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
):
( (
fixed_asset_account, fixed_asset_account,
asset, asset,
@@ -247,28 +249,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
) = get_asset_details(asset, finance_book) ) = get_asset_details(asset, finance_book)
gl_entries = [ gl_entries = [
{ asset.get_gl_dict(
"account": fixed_asset_account, {
"debit_in_account_currency": asset.gross_purchase_amount, "account": fixed_asset_account,
"debit": asset.gross_purchase_amount, "debit_in_account_currency": asset.gross_purchase_amount,
"cost_center": depreciation_cost_center, "debit": asset.gross_purchase_amount,
}, "cost_center": depreciation_cost_center,
{ "posting_date": getdate(),
"account": accumulated_depr_account, },
"credit_in_account_currency": accumulated_depr_amount, item=asset,
"credit": accumulated_depr_amount, ),
"cost_center": depreciation_cost_center, asset.get_gl_dict(
}, {
"account": accumulated_depr_account,
"credit_in_account_currency": accumulated_depr_amount,
"credit": accumulated_depr_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
},
item=asset,
),
] ]
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
if profit_amount: if profit_amount:
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
)
if voucher_type and voucher_no:
for entry in gl_entries:
entry["voucher_type"] = voucher_type
entry["voucher_no"] = voucher_no
return gl_entries return gl_entries
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): def get_gl_entries_on_asset_disposal(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
):
( (
fixed_asset_account, fixed_asset_account,
asset, asset,
@@ -280,23 +299,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
) = get_asset_details(asset, finance_book) ) = get_asset_details(asset, finance_book)
gl_entries = [ gl_entries = [
{ asset.get_gl_dict(
"account": fixed_asset_account, {
"credit_in_account_currency": asset.gross_purchase_amount, "account": fixed_asset_account,
"credit": asset.gross_purchase_amount, "credit_in_account_currency": asset.gross_purchase_amount,
"cost_center": depreciation_cost_center, "credit": asset.gross_purchase_amount,
}, "cost_center": depreciation_cost_center,
{ "posting_date": getdate(),
"account": accumulated_depr_account, },
"debit_in_account_currency": accumulated_depr_amount, item=asset,
"debit": accumulated_depr_amount, ),
"cost_center": depreciation_cost_center, asset.get_gl_dict(
}, {
"account": accumulated_depr_account,
"debit_in_account_currency": accumulated_depr_amount,
"debit": accumulated_depr_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
},
item=asset,
),
] ]
profit_amount = flt(selling_amount) - flt(value_after_depreciation) profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount: if profit_amount:
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
)
if voucher_type and voucher_no:
for entry in gl_entries:
entry["voucher_type"] = voucher_type
entry["voucher_no"] = voucher_no
return gl_entries return gl_entries
@@ -333,15 +367,21 @@ def get_asset_details(asset, finance_book=None):
) )
def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): def get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
):
debit_or_credit = "debit" if profit_amount < 0 else "credit" debit_or_credit = "debit" if profit_amount < 0 else "credit"
gl_entries.append( gl_entries.append(
{ asset.get_gl_dict(
"account": disposal_account, {
"cost_center": depreciation_cost_center, "account": disposal_account,
debit_or_credit: abs(profit_amount), "cost_center": depreciation_cost_center,
debit_or_credit + "_in_account_currency": abs(profit_amount), debit_or_credit: abs(profit_amount),
} debit_or_credit + "_in_account_currency": abs(profit_amount),
"posting_date": getdate(),
},
item=asset,
)
) )

View File

@@ -428,7 +428,11 @@ class AssetCapitalization(StockController):
asset.reload() asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.asset_value, item.get("finance_book") or self.get("finance_book") asset,
item.asset_value,
item.get("finance_book") or self.get("finance_book"),
self.get("doctype"),
self.get("name"),
) )
asset.db_set("disposal_date", self.posting_date) asset.db_set("disposal_date", self.posting_date)

View File

@@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters):
}, },
"type": "line", "type": "line",
"lineOptions": {"regionFill": 1}, "lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
} }

View File

@@ -194,16 +194,16 @@ class BuyingController(SubcontractingController):
if self.meta.get_field("base_in_words"): if self.meta.get_field("base_in_words"):
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled(): if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
amount = self.base_rounded_total amount = abs(self.base_rounded_total)
else: else:
amount = self.base_grand_total amount = abs(self.base_grand_total)
self.base_in_words = money_in_words(amount, self.company_currency) self.base_in_words = money_in_words(amount, self.company_currency)
if self.meta.get_field("in_words"): if self.meta.get_field("in_words"):
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled(): if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
amount = self.rounded_total amount = abs(self.rounded_total)
else: else:
amount = self.grand_total amount = abs(self.grand_total)
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)

View File

@@ -44,7 +44,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = {
}, },
{ {
fieldname: "opportunity_source", fieldname: "opportunity_source",
label: __("Oppoturnity Source"), label: __("Opportunity Source"),
fieldtype: "Link", fieldtype: "Link",
options: "Lead Source", options: "Lead Source",
}, },
@@ -62,4 +62,4 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = {
default: frappe.defaults.get_user_default("Company") default: frappe.defaults.get_user_default("Company")
} }
] ]
}; };

View File

@@ -391,12 +391,12 @@ scheduler_events = {
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
], ],
"hourly": [ "hourly": [
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status", "erpnext.projects.doctype.project.project.collect_project_status",
], ],
"hourly_long": [ "hourly_long": [
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
], ],

View File

@@ -1019,7 +1019,6 @@ def get_bom_items_as_dict(
where where
bom_item.docstatus < 2 bom_item.docstatus < 2
and bom.name = %(bom)s and bom.name = %(bom)s
and ifnull(item.has_variants, 0) = 0
and item.is_stock_item in (1, {is_stock_item}) and item.is_stock_item in (1, {is_stock_item})
{where_conditions} {where_conditions}
group by item_code, stock_uom group by item_code, stock_uom

View File

@@ -894,6 +894,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
.select( .select(
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name, item.item_name,
item.name.as_("item_code"),
bei.description, bei.description,
bei.stock_uom, bei.stock_uom,
item.min_order_qty, item.min_order_qty,

View File

@@ -557,37 +557,52 @@ erpnext.work_order = {
if(!frm.doc.skip_transfer){ if(!frm.doc.skip_transfer){
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing)) if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') {
&& frm.doc.status != 'Stopped') { if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
frm.has_finish_btn = true; frm.has_finish_btn = true;
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
// Only show "Material Consumption" when required_qty > consumed_qty // Only show "Material Consumption" when required_qty > consumed_qty
var counter = 0; var counter = 0;
var tbl = frm.doc.required_items || []; var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length; var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) { for (var i = 0, len = tbl_lenght; i < len; i++) {
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1; counter += 1;
}
}
if (counter > 0) {
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
});
consumption_btn.addClass('btn-primary');
} }
} }
if (counter > 0) {
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { var finish_btn = frm.add_custom_button(__('Finish'), function() {
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; erpnext.work_order.make_se(frm, 'Manufacture');
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); });
});
consumption_btn.addClass('btn-primary'); if(doc.material_transferred_for_manufacturing>=doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
} }
} } else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
var finish_btn = frm.add_custom_button(__('Finish'), function() { if (allowance_percentage > 0) {
erpnext.work_order.make_se(frm, 'Manufacture'); let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
});
if(doc.material_transferred_for_manufacturing>=doc.qty) { if ((flt(doc.produced_qty) < allowed_qty)) {
// all materials transferred for manufacturing, make this primary frm.add_custom_button(__('Finish'), function() {
finish_btn.addClass('btn-primary'); erpnext.work_order.make_se(frm, 'Manufacture');
});
}
}
});
} }
} }
} else { } else {

View File

@@ -64,22 +64,21 @@ def get_columns(filters):
def get_data(filters): def get_data(filters):
cond = "1=1" wo = frappe.qb.DocType("Work Order")
query = (
frappe.qb.from_(wo)
.select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no)
.where((wo.produced_qty > wo.qty) & (wo.docstatus == 1))
)
if filters.get("bom_no") and not filters.get("work_order"): if filters.get("bom_no") and not filters.get("work_order"):
cond += " and bom_no = '%s'" % filters.get("bom_no") query = query.where(wo.bom_no == filters.get("bom_no"))
if filters.get("work_order"): if filters.get("work_order"):
cond += " and name = '%s'" % filters.get("work_order") query = query.where(wo.name == filters.get("work_order"))
results = [] results = []
for d in frappe.db.sql( for d in query.run(as_dict=True):
""" select name as work_order, qty, produced_qty, production_item, bom_no
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
cond
),
as_dict=1,
):
results.append(d) results.append(d)
for data in frappe.get_all( for data in frappe.get_all(
@@ -95,16 +94,17 @@ def get_data(filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_work_orders(doctype, txt, searchfield, start, page_len, filters): def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
cond = "1=1" wo = frappe.qb.DocType("Work Order")
if filters.get("bom_no"): query = (
cond += " and bom_no = '%s'" % filters.get("bom_no") frappe.qb.from_(wo)
.select(wo.name)
return frappe.db.sql( .where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1))
"""select name from `tabWork Order` .orderby(wo.name)
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1 .limit(page_len)
order by name limit {2} offset {1}""".format( .offset(start)
cond, start, page_len
),
{"name": "%%%s%%" % txt},
as_list=1,
) )
if filters.get("bom_no"):
query = query.where(wo.bom_no == filters.get("bom_no"))
return query.run(as_list=True)

View File

@@ -96,38 +96,39 @@ class ForecastingReport(ExponentialSmoothingForecast):
value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty)) value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty))
def get_data_for_forecast(self): def get_data_for_forecast(self):
cond = "" parent = frappe.qb.DocType(self.doctype)
if self.filters.item_code: child = frappe.qb.DocType(self.child_doctype)
cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
warehouses = []
if self.filters.warehouse:
warehouses = get_child_warehouses(self.filters.warehouse)
cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
input_data = [self.filters.from_date, self.filters.company]
if warehouses:
input_data.extend(warehouses)
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
return frappe.db.sql( query = (
""" frappe.qb.from_(parent)
SELECT .from_(child)
so.{date_field} as posting_date, soi.item_code, soi.warehouse, .select(
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount parent[date_field].as_("posting_date"),
FROM child.item_code,
`tab{doc}` so, `tab{child_doc}` soi child.warehouse,
WHERE child.item_name,
so.docstatus = 1 AND so.name = soi.parent AND child.stock_qty.as_("qty"),
so.{date_field} < %s AND so.company = %s {cond} child.base_amount.as_("amount"),
""".format( )
doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond .where(
), (parent.docstatus == 1)
tuple(input_data), & (parent.name == child.parent)
as_dict=1, & (parent[date_field] < self.filters.from_date)
& (parent.company == self.filters.company)
)
) )
if self.filters.item_code:
query = query.where(child.item_code == self.filters.item_code)
if self.filters.warehouse:
warehouses = get_child_warehouses(self.filters.warehouse) or []
query = query.where(child.warehouse.isin(warehouses))
return query.run(as_dict=True)
def prepare_final_data(self): def prepare_final_data(self):
self.data = [] self.data = []

View File

@@ -85,8 +85,8 @@ def get_chart_data(job_card_details, filters):
open_job_cards.append(periodic_data.get("Open").get(d)) open_job_cards.append(periodic_data.get("Open").get(d))
completed.append(periodic_data.get("Completed").get(d)) completed.append(periodic_data.get("Completed").get(d))
datasets.append({"name": "Open", "values": open_job_cards}) datasets.append({"name": _("Open"), "values": open_job_cards})
datasets.append({"name": "Completed", "values": completed}) datasets.append({"name": _("Completed"), "values": completed})
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}

View File

@@ -4,42 +4,10 @@
import frappe import frappe
from frappe import _ from frappe import _
from pypika import Order
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
# and bom_no is not null and bom_no !=''
mapper = {
"Sales Order": {
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
`tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
"filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
and `tabSales Order`.per_delivered < 100.0""",
},
"Material Request": {
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
`tabMaterial Request Item`.schedule_date """,
"filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
and `tabMaterial Request`.material_request_type = 'Manufacture' """,
},
"Work Order": {
"fields": """ production_item, item_name as production_item_name, planned_start_date,
stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
},
}
order_mapper = {
"Sales Order": {
"Delivery Date": "`tabSales Order Item`.delivery_date asc",
"Total Amount": "`tabSales Order`.base_grand_total desc",
},
"Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
"Work Order": {"Planned Start Date": "planned_start_date asc"},
}
def execute(filters=None): def execute(filters=None):
return ProductionPlanReport(filters).execute_report() return ProductionPlanReport(filters).execute_report()
@@ -63,40 +31,78 @@ class ProductionPlanReport(object):
return self.columns, self.data return self.columns, self.data
def get_open_orders(self): def get_open_orders(self):
doctype = ( doctype, order_by = self.filters.based_on, self.filters.order_by
"`tabWork Order`"
if self.filters.based_on == "Work Order"
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
)
filters = mapper.get(self.filters.based_on)["filters"] parent = frappe.qb.DocType(doctype)
filters = self.prepare_other_conditions(filters, self.filters.based_on) query = None
order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
self.orders = frappe.db.sql( if doctype == "Work Order":
""" SELECT {fields} from {doctype} query = (
WHERE {filters} {order_by}""".format( frappe.qb.from_(parent)
doctype=doctype, .select(
filters=filters, parent.production_item,
order_by=order_by, parent.item_name.as_("production_item_name"),
fields=mapper.get(self.filters.based_on)["fields"], parent.planned_start_date,
), parent.stock_uom,
tuple(self.filters.docnames), parent.qty.as_("qty_to_manufacture"),
as_dict=1, parent.name,
) parent.bom_no,
parent.fg_warehouse.as_("warehouse"),
)
.where(parent.status.notin(["Completed", "Stopped"]))
)
def prepare_other_conditions(self, filters, doctype): if order_by == "Planned Start Date":
if self.filters.docnames: query = query.orderby(parent.planned_start_date, order=Order.asc)
field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
if doctype != "Work Order": if self.filters.docnames:
filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype) query = query.where(parent.name.isin(self.filters.docnames))
else:
child = frappe.qb.DocType(f"{doctype} Item")
query = (
frappe.qb.from_(parent)
.from_(child)
.select(
child.bom_no,
child.stock_uom,
child.warehouse,
child.parent.as_("name"),
child.item_code.as_("production_item"),
child.stock_qty.as_("qty_to_manufacture"),
child.item_name.as_("production_item_name"),
)
.where(parent.name == child.parent)
)
if self.filters.docnames:
query = query.where(child.parent.isin(self.filters.docnames))
if doctype == "Sales Order":
query = query.select(
child.delivery_date,
parent.base_grand_total,
).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0))
if order_by == "Delivery Date":
query = query.orderby(child.delivery_date, order=Order.asc)
elif order_by == "Total Amount":
query = query.orderby(parent.base_grand_total, order=Order.desc)
elif doctype == "Material Request":
query = query.select(child.schedule_date,).where(
(parent.per_ordered < 100) & (parent.material_request_type == "Manufacture")
)
if order_by == "Required Date":
query = query.orderby(child.schedule_date, order=Order.asc)
query = query.where(parent.docstatus == 1)
if self.filters.company: if self.filters.company:
filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company)) query = query.where(parent.company == self.filters.company)
return filters self.orders = query.run(as_dict=True)
def get_raw_materials(self): def get_raw_materials(self):
if not self.orders: if not self.orders:
@@ -134,29 +140,29 @@ class ProductionPlanReport(object):
bom_nos.append(bom_no) bom_nos.append(bom_no)
bom_doctype = ( bom_item_doctype = (
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item" "BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
) )
qty_field = ( bom = frappe.qb.DocType("BOM")
"qty_consumed_per_unit" bom_item = frappe.qb.DocType(bom_item_doctype)
if self.filters.include_subassembly_raw_materials
else "(bom_item.qty / bom.quantity)"
)
raw_materials = frappe.db.sql( if self.filters.include_subassembly_raw_materials:
""" SELECT bom_item.parent, bom_item.item_code, qty_field = bom_item.qty_consumed_per_unit
bom_item.item_name as raw_material_name, {0} as required_qty_per_unit else:
FROM qty_field = bom_item.qty / bom.quantity
`tabBOM` as bom, `tab{1}` as bom_item
WHERE raw_materials = (
bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1 frappe.qb.from_(bom)
""".format( .from_(bom_item)
qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos)) .select(
), bom_item.parent,
tuple(bom_nos), bom_item.item_code,
as_dict=1, bom_item.item_name.as_("raw_material_name"),
) qty_field.as_("required_qty_per_unit"),
)
.where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1))
).run(as_dict=True)
if not raw_materials: if not raw_materials:
return return

View File

@@ -83,6 +83,7 @@ def get_chart_based_on_status(data):
for d in data: for d in data:
status_wise_data[d.status] += 1 status_wise_data[d.status] += 1
labels = [_(label) for label in labels]
values = [status_wise_data[label] for label in labels] values = [status_wise_data[label] for label in labels]
chart = { chart = {
@@ -95,7 +96,7 @@ def get_chart_based_on_status(data):
def get_chart_based_on_age(data): def get_chart_based_on_age(data):
labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] labels = [_("0-30 Days"), _("30-60 Days"), _("60-90 Days"), _("90 Above")]
age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0} age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0}
@@ -135,8 +136,8 @@ def get_chart_based_on_qty(data, filters):
pending.append(periodic_data.get("Pending").get(d)) pending.append(periodic_data.get("Pending").get(d))
completed.append(periodic_data.get("Completed").get(d)) completed.append(periodic_data.get("Completed").get(d))
datasets.append({"name": "Pending", "values": pending}) datasets.append({"name": _("Pending"), "values": pending})
datasets.append({"name": "Completed", "values": completed}) datasets.append({"name": _("Completed"), "values": completed})
chart = { chart = {
"data": {"labels": labels, "datasets": datasets}, "data": {"labels": labels, "datasets": datasets},

View File

@@ -91,9 +91,9 @@ def get_chart_data(data):
"data": { "data": {
"labels": labels[:30], "labels": labels[:30],
"datasets": [ "datasets": [
{"name": "Overdue", "values": overdue[:30]}, {"name": _("Overdue"), "values": overdue[:30]},
{"name": "Completed", "values": completed[:30]}, {"name": _("Completed"), "values": completed[:30]},
{"name": "Total Tasks", "values": total[:30]}, {"name": _("Total Tasks"), "values": total[:30]},
], ],
}, },
"type": "bar", "type": "bar",

View File

@@ -660,7 +660,7 @@ erpnext.PointOfSale.Controller = class {
} else { } else {
return; return;
} }
} else if (available_qty < qty_needed) { } else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({ frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange' indicator: 'orange'
@@ -694,7 +694,7 @@ erpnext.PointOfSale.Controller = class {
callback(res) { callback(res) {
if (!me.item_stock_map[item_code]) if (!me.item_stock_map[item_code])
me.item_stock_map[item_code] = {}; me.item_stock_map[item_code] = {};
me.item_stock_map[item_code][warehouse] = res.message[0]; me.item_stock_map[item_code][warehouse] = res.message;
} }
}); });
} }

View File

@@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class {
if (this.value) { if (this.value) {
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map(); me.item_stock_map = me.events.get_item_stock_map();
const available_qty = me.item_stock_map[me.item_row.item_code] && me.item_stock_map[me.item_row.item_code][this.value]; const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0];
const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]);
if (available_qty === undefined) { if (available_qty === undefined) {
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
// item stock map is updated now reset warehouse // item stock map is updated now reset warehouse
me.warehouse_control.set_value(this.value); me.warehouse_control.set_value(this.value);
}) })
} else if (available_qty === 0) { } else if (available_qty === 0 && is_stock_item) {
me.warehouse_control.set_value(''); me.warehouse_control.set_value('');
const bold_item_code = me.item_row.item_code.bold(); const bold_item_code = me.item_row.item_code.bold();
const bold_warehouse = this.value.bold(); const bold_warehouse = this.value.bold();

View File

@@ -1241,6 +1241,37 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(query[0].value, 0) self.assertEqual(query[0].value, 0)
def test_batch_expiry_for_purchase_receipt(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pi = make_purchase_receipt(
qty=1,
item_code=item.name,
update_stock=True,
)
pi.load_from_db()
batch_no = pi.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save().submit()
self.assertTrue(return_pi.docstatus == 1)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -1073,8 +1073,8 @@ class StockEntry(StockController):
# No work order could mean independent Manufacture entry, if so skip validation # No work order could mean independent Manufacture entry, if so skip validation
if self.work_order and self.fg_completed_qty > allowed_qty: if self.work_order and self.fg_completed_qty > allowed_qty:
frappe.throw( frappe.throw(
_("For quantity {0} should not be greater than work order quantity {1}").format( _("For quantity {0} should not be greater than allowed quantity {1}").format(
flt(self.fg_completed_qty), wo_qty flt(self.fg_completed_qty), allowed_qty
) )
) )

View File

@@ -153,6 +153,9 @@ class StockLedgerEntry(Document):
def validate_batch(self): def validate_batch(self):
if self.batch_no and self.voucher_type != "Stock Entry": if self.batch_no and self.voucher_type != "Stock Entry":
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0:
return
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")
if expiry_date: if expiry_date:
if getdate(self.posting_date) > getdate(expiry_date): if getdate(self.posting_date) > getdate(expiry_date):

View File

@@ -45,4 +45,5 @@ def get_chart_data(data, filters):
"datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}],
}, },
"type": "bar", "type": "bar",
"fieldtype": "Currency",
} }

View File

@@ -4,6 +4,8 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import Field
from frappe.query_builder.functions import Min, Timestamp
from frappe.utils import add_days, getdate, today from frappe.utils import add_days, getdate, today
import erpnext import erpnext
@@ -28,7 +30,7 @@ def execute(filters=None):
def get_unsync_date(filters): def get_unsync_date(filters):
date = filters.from_date date = filters.from_date
if not date: if not date:
date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run()
date = date[0][0] date = date[0][0]
if not date: if not date:
@@ -54,22 +56,27 @@ def get_data(report_filters):
result = [] result = []
voucher_wise_dict = {} voucher_wise_dict = {}
data = frappe.db.sql( sle = frappe.qb.DocType("Stock Ledger Entry")
""" data = (
SELECT frappe.qb.from_(sle)
name, posting_date, posting_time, voucher_type, voucher_no, .select(
stock_value_difference, stock_value, warehouse, item_code sle.name,
FROM sle.posting_date,
`tabStock Ledger Entry` sle.posting_time,
WHERE sle.voucher_type,
posting_date sle.voucher_no,
= %s and company = %s sle.stock_value_difference,
and is_cancelled = 0 sle.stock_value,
ORDER BY timestamp(posting_date, posting_time) asc, creation asc sle.warehouse,
""", sle.item_code,
(from_date, report_filters.company), )
as_dict=1, .where(
) (sle.posting_date == from_date)
& (sle.company == report_filters.company)
& (sle.is_cancelled == 0)
)
.orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation)
).run(as_dict=True)
for d in data: for d in data:
voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)

View File

@@ -62,22 +62,28 @@ def get_data(filters, columns):
def get_item_price_qty_data(filters): def get_item_price_qty_data(filters):
conditions = "" item_price = frappe.qb.DocType("Item Price")
if filters.get("item_code"): bin = frappe.qb.DocType("Bin")
conditions += "where a.item_code=%(item_code)s"
item_results = frappe.db.sql( query = (
"""select a.item_code, a.item_name, a.name as price_list_name, frappe.qb.from_(item_price)
a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty .left_join(bin)
from `tabItem Price` a left join `tabBin` b .on(item_price.item_code == bin.item_code)
ON a.item_code = b.item_code .select(
{conditions}""".format( item_price.item_code,
conditions=conditions item_price.item_name,
), item_price.name.as_("price_list_name"),
filters, item_price.brand.as_("brand"),
as_dict=1, bin.warehouse.as_("warehouse"),
bin.actual_qty.as_("actual_qty"),
)
) )
if filters.get("item_code"):
query = query.where(item_price.item_code == filters.get("item_code"))
item_results = query.run(as_dict=True)
price_list_names = list(set(item.price_list_name for item in item_results)) price_list_names = list(set(item.price_list_name for item in item_results))
buying_price_map = get_price_map(price_list_names, buying=1) buying_price_map = get_price_map(price_list_names, buying=1)

View File

@@ -46,4 +46,5 @@ def get_chart_data(data, filters):
}, },
"type": "bar", "type": "bar",
"colors": ["#5e64ff"], "colors": ["#5e64ff"],
"fieldtype": "Currency",
} }