Merge branch 'version-13-hotfix' into fix-payment-reconciliation-jv-cost-center

This commit is contained in:
Deepesh Garg
2022-10-01 11:10:21 +05:30
committed by GitHub
22 changed files with 219 additions and 93 deletions

View File

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

View File

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

View File

@@ -1549,6 +1549,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.save()
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):
gl_entries = frappe.db.sql(

View File

@@ -326,6 +326,9 @@ def get_advance_vouchers(
"party": ["in", parties],
}
if party_type == "Customer":
filters.update({"against_voucher": ["is", "not set"]})
if company:
filters["company"] = company
if from_date and to_date:

View File

@@ -280,9 +280,9 @@ def get_conditions(filters):
or filters.get("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"):
conditions.append("project in %(project)s")

View File

@@ -19,14 +19,19 @@ def execute(filters=None):
return _execute(filters)
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
def _execute(
filters=None,
additional_table_columns=None,
additional_query_columns=None,
additional_conditions=None,
):
if not filters:
filters = {}
columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_query_columns)
item_list = get_items(filters, additional_query_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
return columns
def get_conditions(filters):
def get_conditions(filters, additional_conditions=None):
conditions = ""
for opts in (
@@ -341,6 +346,9 @@ def get_conditions(filters):
if filters.get(opts[0]):
conditions += opts[1]
if additional_conditions:
conditions += additional_conditions
if filters.get("mode_of_payment"):
conditions += """ and exists(select name from `tabSales Invoice Payment`
where parent=`tabSales Invoice`.name
@@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters, additional_conditions)
if additional_query_columns:
additional_query_columns = ", " + ", ".join(additional_query_columns)

View File

@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
query_filters = {
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"report_type": report_type,
"year_start_date": filters.year_start_date,
"project": filters.project,
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
where
company=%(company)s
{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 is_cancelled = 0
group by account""".format(

View File

@@ -106,12 +106,17 @@ def get_opening_balances(filters):
where company=%(company)s
and is_cancelled=0
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}
group by party""".format(
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,
)

View File

@@ -478,7 +478,6 @@ scheduler_events = {
],
"hourly": [
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
@@ -487,6 +486,7 @@ scheduler_events = {
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
],

View File

@@ -181,7 +181,6 @@ def add_data(
total_l += 1
elif status == "Half Day":
total_p += 0.5
total_a += 0.5
total_l += 0.5
elif not status:
total_um += 1

View File

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

View File

@@ -100,6 +100,7 @@ def execute():
"mode_of_payment": loan.mode_of_payment,
"loan_account": loan.loan_account,
"payment_account": loan.payment_account,
"disbursement_account": loan.payment_account,
"interest_income_account": loan.interest_income_account,
"penalty_income_account": loan.penalty_income_account,
},
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account

View File

@@ -15,12 +15,6 @@ filters = filters.concat({
"placeholder":"Company GSTIN",
"options": [""],
"width": "80"
}, {
"fieldname":"invoice_type",
"label": __("Invoice Type"),
"fieldtype": "Select",
"placeholder":"Invoice Type",
"options": ["", "Regular", "SEZ", "Export", "Deemed Export"]
});
// Handle company on change

View File

@@ -5,31 +5,48 @@
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import _execute
def get_conditions(filters, additional_query_columns):
conditions = ""
for opts in additional_query_columns:
if filters.get(opts):
conditions += f" and {opts}=%({opts})s"
return conditions
def execute(filters=None):
additional_table_columns = [
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
dict(
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
),
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
]
additional_query_columns = [
"customer_gstin",
"billing_address_gstin",
"company_gstin",
"place_of_supply",
"reverse_charge",
"gst_category",
"export_type",
"ecommerce_gstin",
"gst_hsn_code",
]
additional_conditions = get_conditions(filters, additional_query_columns)
return _execute(
filters,
additional_table_columns=[
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
dict(
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
),
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
],
additional_query_columns=[
"customer_gstin",
"billing_address_gstin",
"company_gstin",
"place_of_supply",
"reverse_charge",
"gst_category",
"export_type",
"ecommerce_gstin",
"gst_hsn_code",
],
additional_table_columns=additional_table_columns,
additional_query_columns=additional_query_columns,
additional_conditions=additional_conditions,
)

View File

@@ -660,7 +660,7 @@ erpnext.PointOfSale.Controller = class {
} else {
return;
}
} else if (available_qty < qty_needed) {
} else if (is_stock_item && available_qty < qty_needed) {
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]),
indicator: 'orange'
@@ -694,7 +694,7 @@ erpnext.PointOfSale.Controller = class {
callback(res) {
if (!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) {
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map();
const available_qty = 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) {
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
// item stock map is updated now reset warehouse
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('');
const bold_item_code = me.item_row.item_code.bold();
const bold_warehouse = this.value.bold();

View File

@@ -1446,6 +1446,37 @@ class TestPurchaseReceipt(FrappeTestCase):
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():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

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

View File

@@ -153,6 +153,9 @@ class StockLedgerEntry(Document):
def validate_batch(self):
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")
if expiry_date:
if getdate(self.posting_date) > getdate(expiry_date):

View File

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

View File

@@ -18,7 +18,7 @@
<b class="caret"></b>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% if doc.doctype == 'Purchase Order' %}
{% if doc.doctype == 'Purchase Order' and show_make_pi_button %}
<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
{% endif %}
<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'

View File

@@ -53,6 +53,9 @@ def get_context(context):
)
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
# show Make Purchase Invoice button based on permission
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
def get_attachments(dt, dn):
return frappe.get_all(